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 /docshell | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
820 files changed, 83052 insertions, 0 deletions
diff --git a/docshell/base/BaseHistory.cpp b/docshell/base/BaseHistory.cpp new file mode 100644 index 0000000000..a38fb71657 --- /dev/null +++ b/docshell/base/BaseHistory.cpp @@ -0,0 +1,246 @@ +/* -*- 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/. */ + +#include "BaseHistory.h" +#include "nsThreadUtils.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Link.h" +#include "mozilla/dom/Element.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_layout.h" + +namespace mozilla { + +using mozilla::dom::ContentParent; +using mozilla::dom::Link; + +BaseHistory::BaseHistory() : mTrackedURIs(kTrackedUrisInitialSize) {} + +BaseHistory::~BaseHistory() = default; + +static constexpr nsLiteralCString kDisallowedSchemes[] = { + "about"_ns, "blob"_ns, "data"_ns, "chrome"_ns, + "imap"_ns, "javascript"_ns, "mailbox"_ns, "moz-anno"_ns, + "news"_ns, "page-icon"_ns, "resource"_ns, "view-source"_ns, + "moz-extension"_ns, +}; + +bool BaseHistory::CanStore(nsIURI* aURI) { + nsAutoCString scheme; + if (NS_WARN_IF(NS_FAILED(aURI->GetScheme(scheme)))) { + return false; + } + + if (!scheme.EqualsLiteral("http") && !scheme.EqualsLiteral("https")) { + for (const nsLiteralCString& disallowed : kDisallowedSchemes) { + if (scheme.Equals(disallowed)) { + return false; + } + } + } + + nsAutoCString spec; + aURI->GetSpec(spec); + return spec.Length() <= StaticPrefs::browser_history_maxUrlLength(); +} + +void BaseHistory::ScheduleVisitedQuery(nsIURI* aURI, + dom::ContentParent* aForProcess) { + mPendingQueries.WithEntryHandle(aURI, [&](auto&& entry) { + auto& set = entry.OrInsertWith([] { return ContentParentSet(); }); + if (aForProcess) { + set.Insert(aForProcess); + } + }); + if (mStartPendingVisitedQueriesScheduled) { + return; + } + mStartPendingVisitedQueriesScheduled = + NS_SUCCEEDED(NS_DispatchToMainThreadQueue( + NS_NewRunnableFunction( + "BaseHistory::StartPendingVisitedQueries", + [self = RefPtr<BaseHistory>(this)] { + self->mStartPendingVisitedQueriesScheduled = false; + auto queries = std::move(self->mPendingQueries); + self->StartPendingVisitedQueries(std::move(queries)); + MOZ_DIAGNOSTIC_ASSERT(self->mPendingQueries.IsEmpty()); + }), + EventQueuePriority::Idle)); +} + +void BaseHistory::CancelVisitedQueryIfPossible(nsIURI* aURI) { + mPendingQueries.Remove(aURI); + // TODO(bug 1591393): It could be worth to make this virtual and allow places + // to stop the existing database query? Needs some measurement. +} + +void BaseHistory::RegisterVisitedCallback(nsIURI* aURI, Link* aLink) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aURI, "Must pass a non-null URI!"); + MOZ_ASSERT(aLink, "Must pass a non-null Link!"); + + if (!CanStore(aURI)) { + aLink->VisitedQueryFinished(/* visited = */ false); + return; + } + + // Obtain our array of observers for this URI. + auto* const links = + mTrackedURIs.WithEntryHandle(aURI, [&](auto&& entry) -> ObservingLinks* { + MOZ_DIAGNOSTIC_ASSERT(!entry || !entry->mLinks.IsEmpty(), + "An empty key was kept around in our hashtable!"); + if (!entry) { + ScheduleVisitedQuery(aURI, nullptr); + } + + return &entry.OrInsertWith([] { return ObservingLinks{}; }); + }); + + if (!links) { + return; + } + + // Sanity check that Links are not registered more than once for a given URI. + // This will not catch a case where it is registered for two different URIs. + MOZ_DIAGNOSTIC_ASSERT(!links->mLinks.Contains(aLink), + "Already tracking this Link object!"); + + links->mLinks.AppendElement(aLink); + + // If this link has already been queried and we should notify, do so now. + switch (links->mStatus) { + case VisitedStatus::Unknown: + break; + case VisitedStatus::Unvisited: + [[fallthrough]]; + case VisitedStatus::Visited: + aLink->VisitedQueryFinished(links->mStatus == VisitedStatus::Visited); + break; + } +} + +void BaseHistory::UnregisterVisitedCallback(nsIURI* aURI, Link* aLink) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aURI, "Must pass a non-null URI!"); + MOZ_ASSERT(aLink, "Must pass a non-null Link object!"); + + // Get the array, and remove the item from it. + auto entry = mTrackedURIs.Lookup(aURI); + if (!entry) { + MOZ_ASSERT(!CanStore(aURI), + "Trying to unregister URI that wasn't registered, " + "and that could be visited!"); + return; + } + + ObserverArray& observers = entry->mLinks; + if (!observers.RemoveElement(aLink)) { + MOZ_ASSERT_UNREACHABLE("Trying to unregister node that wasn't registered!"); + return; + } + + // If the array is now empty, we should remove it from the hashtable. + if (observers.IsEmpty()) { + entry.Remove(); + CancelVisitedQueryIfPossible(aURI); + } +} + +void BaseHistory::NotifyVisited( + nsIURI* aURI, VisitedStatus aStatus, + const ContentParentSet* aListOfProcessesToNotify) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aStatus != VisitedStatus::Unknown); + + NotifyVisitedInThisProcess(aURI, aStatus); + if (XRE_IsParentProcess()) { + NotifyVisitedFromParent(aURI, aStatus, aListOfProcessesToNotify); + } +} + +void BaseHistory::NotifyVisitedInThisProcess(nsIURI* aURI, + VisitedStatus aStatus) { + if (NS_WARN_IF(!aURI)) { + return; + } + + auto entry = mTrackedURIs.Lookup(aURI); + if (!entry) { + // If we have no observers for this URI, we have nothing to notify about. + return; + } + + ObservingLinks& links = entry.Data(); + links.mStatus = aStatus; + + // If we have a key, it should have at least one observer. + MOZ_ASSERT(!links.mLinks.IsEmpty()); + + // Dispatch an event to each document which has a Link observing this URL. + // These will fire asynchronously in the correct DocGroup. + + const bool visited = aStatus == VisitedStatus::Visited; + for (Link* link : links.mLinks.BackwardRange()) { + link->VisitedQueryFinished(visited); + } +} + +void BaseHistory::SendPendingVisitedResultsToChildProcesses() { + MOZ_ASSERT(!mPendingResults.IsEmpty()); + + mStartPendingResultsScheduled = false; + + auto results = std::move(mPendingResults); + MOZ_ASSERT(mPendingResults.IsEmpty()); + + nsTArray<ContentParent*> cplist; + nsTArray<dom::VisitedQueryResult> resultsForProcess; + ContentParent::GetAll(cplist); + for (ContentParent* cp : cplist) { + resultsForProcess.ClearAndRetainStorage(); + for (auto& result : results) { + if (result.mProcessesToNotify.IsEmpty() || + result.mProcessesToNotify.Contains(cp)) { + resultsForProcess.AppendElement(result.mResult); + } + } + if (!resultsForProcess.IsEmpty()) { + Unused << NS_WARN_IF(!cp->SendNotifyVisited(resultsForProcess)); + } + } +} + +void BaseHistory::NotifyVisitedFromParent( + nsIURI* aURI, VisitedStatus aStatus, + const ContentParentSet* aListOfProcessesToNotify) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (aListOfProcessesToNotify && aListOfProcessesToNotify->IsEmpty()) { + return; + } + + auto& result = *mPendingResults.AppendElement(); + result.mResult.visited() = aStatus == VisitedStatus::Visited; + result.mResult.uri() = aURI; + if (aListOfProcessesToNotify) { + for (auto* entry : *aListOfProcessesToNotify) { + result.mProcessesToNotify.Insert(entry); + } + } + + if (mStartPendingResultsScheduled) { + return; + } + + mStartPendingResultsScheduled = NS_SUCCEEDED(NS_DispatchToMainThreadQueue( + NewRunnableMethod( + "BaseHistory::SendPendingVisitedResultsToChildProcesses", this, + &BaseHistory::SendPendingVisitedResultsToChildProcesses), + EventQueuePriority::Idle)); +} + +} // namespace mozilla diff --git a/docshell/base/BaseHistory.h b/docshell/base/BaseHistory.h new file mode 100644 index 0000000000..f0fa36db99 --- /dev/null +++ b/docshell/base/BaseHistory.h @@ -0,0 +1,85 @@ +/* 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/. */ + +#ifndef mozilla_BaseHistory_h +#define mozilla_BaseHistory_h + +#include "IHistory.h" +#include "mozilla/dom/ContentParent.h" +#include "nsTHashSet.h" + +/* A base class for history implementations that implement link coloring. */ + +namespace mozilla { + +class BaseHistory : public IHistory { + public: + void RegisterVisitedCallback(nsIURI*, dom::Link*) final; + void ScheduleVisitedQuery(nsIURI*, dom::ContentParent*) final; + void UnregisterVisitedCallback(nsIURI*, dom::Link*) final; + void NotifyVisited(nsIURI*, VisitedStatus, + const ContentParentSet* = nullptr) final; + + // Some URIs like data-uris are never going to be stored in history, so we can + // avoid doing IPC roundtrips for them or what not. + static bool CanStore(nsIURI*); + + protected: + void NotifyVisitedInThisProcess(nsIURI*, VisitedStatus); + void NotifyVisitedFromParent(nsIURI*, VisitedStatus, const ContentParentSet*); + static constexpr const size_t kTrackedUrisInitialSize = 64; + + BaseHistory(); + ~BaseHistory(); + + using ObserverArray = nsTObserverArray<dom::Link*>; + struct ObservingLinks { + ObserverArray mLinks; + VisitedStatus mStatus = VisitedStatus::Unknown; + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + return mLinks.ShallowSizeOfExcludingThis(aMallocSizeOf); + } + }; + + using PendingVisitedQueries = nsTHashMap<nsURIHashKey, ContentParentSet>; + struct PendingVisitedResult { + dom::VisitedQueryResult mResult; + ContentParentSet mProcessesToNotify; + }; + using PendingVisitedResults = nsTArray<PendingVisitedResult>; + + // Starts all the queries in the pending queries list, potentially at the same + // time. + virtual void StartPendingVisitedQueries(PendingVisitedQueries&&) = 0; + + private: + // Cancels a visited query, if it is at all possible, because we know we won't + // use the results anymore. + void CancelVisitedQueryIfPossible(nsIURI*); + + void SendPendingVisitedResultsToChildProcesses(); + + protected: + // A map from URI to links that depend on that URI, and whether that URI is + // known-to-be-visited-or-unvisited already. + nsTHashMap<nsURIHashKey, ObservingLinks> mTrackedURIs; + + private: + // The set of pending URIs that we haven't queried yet but need to. + PendingVisitedQueries mPendingQueries; + // The set of pending query results that we still haven't dispatched to child + // processes. + PendingVisitedResults mPendingResults; + // Whether we've successfully scheduled a runnable to call + // StartPendingVisitedQueries already. + bool mStartPendingVisitedQueriesScheduled = false; + // Whether we've successfully scheduled a runnable to call + // SendPendingVisitedResultsToChildProcesses already. + bool mStartPendingResultsScheduled = false; +}; + +} // namespace mozilla + +#endif diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp new file mode 100644 index 0000000000..e515c4e9aa --- /dev/null +++ b/docshell/base/BrowsingContext.cpp @@ -0,0 +1,3832 @@ +/* -*- 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/. */ + +#include "mozilla/dom/BrowsingContext.h" + +#include "ipc/IPCMessageUtils.h" + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/DocAccessibleParent.h" +# include "mozilla/a11y/Platform.h" +# include "mozilla/a11y/RemoteAccessibleBase.h" +# include "nsAccessibilityService.h" +# if defined(XP_WIN) +# include "mozilla/a11y/AccessibleWrap.h" +# include "mozilla/a11y/Compatibility.h" +# include "mozilla/a11y/nsWinUtils.h" +# endif +#endif +#include "mozilla/AppShutdown.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/BrowserHost.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/BrowsingContextGroup.h" +#include "mozilla/dom/BrowsingContextBinding.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLEmbedElement.h" +#include "mozilla/dom/HTMLIFrameElement.h" +#include "mozilla/dom/Location.h" +#include "mozilla/dom/LocationBinding.h" +#include "mozilla/dom/MediaDevices.h" +#include "mozilla/dom/PopupBlocker.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SessionStoreChild.h" +#include "mozilla/dom/SessionStorageManager.h" +#include "mozilla/dom/StructuredCloneTags.h" +#include "mozilla/dom/UserActivationIPCUtils.h" +#include "mozilla/dom/WindowBinding.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "mozilla/dom/SyncedContextInlines.h" +#include "mozilla/dom/XULFrameElement.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/net/DocumentLoadListener.h" +#include "mozilla/net/RequestContextService.h" +#include "mozilla/Assertions.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Components.h" +#include "mozilla/HashTable.h" +#include "mozilla/Logging.h" +#include "mozilla/MediaFeatureChange.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/StaticPrefs_page_load.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/URLQueryStringStripper.h" +#include "mozilla/EventStateManager.h" +#include "nsIURIFixup.h" +#include "nsIXULRuntime.h" + +#include "nsDocShell.h" +#include "nsDocShellLoadState.h" +#include "nsFocusManager.h" +#include "nsGlobalWindowOuter.h" +#include "PresShell.h" +#include "nsIObserverService.h" +#include "nsISHistory.h" +#include "nsContentUtils.h" +#include "nsQueryObject.h" +#include "nsSandboxFlags.h" +#include "nsScriptError.h" +#include "nsThreadUtils.h" +#include "xpcprivate.h" + +#include "AutoplayPolicy.h" +#include "GVAutoplayRequestStatusIPC.h" + +extern mozilla::LazyLogModule gAutoplayPermissionLog; +extern mozilla::LazyLogModule gTimeoutDeferralLog; + +#define AUTOPLAY_LOG(msg, ...) \ + MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +namespace IPC { +// Allow serialization and deserialization of OrientationType over IPC +template <> +struct ParamTraits<mozilla::dom::OrientationType> + : public ContiguousEnumSerializer< + mozilla::dom::OrientationType, + mozilla::dom::OrientationType::Portrait_primary, + mozilla::dom::OrientationType::EndGuard_> {}; + +template <> +struct ParamTraits<mozilla::dom::DisplayMode> + : public ContiguousEnumSerializer<mozilla::dom::DisplayMode, + mozilla::dom::DisplayMode::Browser, + mozilla::dom::DisplayMode::EndGuard_> {}; + +template <> +struct ParamTraits<mozilla::dom::PrefersColorSchemeOverride> + : public ContiguousEnumSerializer< + mozilla::dom::PrefersColorSchemeOverride, + mozilla::dom::PrefersColorSchemeOverride::None, + mozilla::dom::PrefersColorSchemeOverride::EndGuard_> {}; + +template <> +struct ParamTraits<mozilla::dom::ExplicitActiveStatus> + : public ContiguousEnumSerializer< + mozilla::dom::ExplicitActiveStatus, + mozilla::dom::ExplicitActiveStatus::None, + mozilla::dom::ExplicitActiveStatus::EndGuard_> {}; + +// Allow serialization and deserialization of TouchEventsOverride over IPC +template <> +struct ParamTraits<mozilla::dom::TouchEventsOverride> + : public ContiguousEnumSerializer< + mozilla::dom::TouchEventsOverride, + mozilla::dom::TouchEventsOverride::Disabled, + mozilla::dom::TouchEventsOverride::EndGuard_> {}; + +template <> +struct ParamTraits<mozilla::dom::EmbedderColorSchemes> { + using paramType = mozilla::dom::EmbedderColorSchemes; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mUsed); + WriteParam(aWriter, aParam.mPreferred); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mUsed) && + ReadParam(aReader, &aResult->mPreferred); + } +}; + +} // namespace IPC + +namespace mozilla { +namespace dom { + +// Explicit specialization of the `Transaction` type. Required by the `extern +// template class` declaration in the header. +template class syncedcontext::Transaction<BrowsingContext>; + +extern mozilla::LazyLogModule gUserInteractionPRLog; + +#define USER_ACTIVATION_LOG(msg, ...) \ + MOZ_LOG(gUserInteractionPRLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +static LazyLogModule gBrowsingContextLog("BrowsingContext"); +static LazyLogModule gBrowsingContextSyncLog("BrowsingContextSync"); + +typedef nsTHashMap<nsUint64HashKey, BrowsingContext*> BrowsingContextMap; + +// All BrowsingContexts indexed by Id +static StaticAutoPtr<BrowsingContextMap> sBrowsingContexts; +// Top-level Content BrowsingContexts only, indexed by BrowserId instead of Id +static StaticAutoPtr<BrowsingContextMap> sCurrentTopByBrowserId; + +static void UnregisterBrowserId(BrowsingContext* aBrowsingContext) { + if (!aBrowsingContext->IsTopContent() || !sCurrentTopByBrowserId) { + return; + } + + // Avoids an extra lookup + auto browserIdEntry = + sCurrentTopByBrowserId->Lookup(aBrowsingContext->BrowserId()); + if (browserIdEntry && browserIdEntry.Data() == aBrowsingContext) { + browserIdEntry.Remove(); + } +} + +static void Register(BrowsingContext* aBrowsingContext) { + sBrowsingContexts->InsertOrUpdate(aBrowsingContext->Id(), aBrowsingContext); + if (aBrowsingContext->IsTopContent()) { + sCurrentTopByBrowserId->InsertOrUpdate(aBrowsingContext->BrowserId(), + aBrowsingContext); + } + + aBrowsingContext->Group()->Register(aBrowsingContext); +} + +// static +void BrowsingContext::UpdateCurrentTopByBrowserId( + BrowsingContext* aNewBrowsingContext) { + if (aNewBrowsingContext->IsTopContent()) { + sCurrentTopByBrowserId->InsertOrUpdate(aNewBrowsingContext->BrowserId(), + aNewBrowsingContext); + } +} + +BrowsingContext* BrowsingContext::GetParent() const { + return mParentWindow ? mParentWindow->GetBrowsingContext() : nullptr; +} + +bool BrowsingContext::IsInSubtreeOf(BrowsingContext* aContext) { + BrowsingContext* bc = this; + do { + if (bc == aContext) { + return true; + } + } while ((bc = bc->GetParent())); + return false; +} + +BrowsingContext* BrowsingContext::Top() { + BrowsingContext* bc = this; + while (bc->mParentWindow) { + bc = bc->GetParent(); + } + return bc; +} + +const BrowsingContext* BrowsingContext::Top() const { + const BrowsingContext* bc = this; + while (bc->mParentWindow) { + bc = bc->GetParent(); + } + return bc; +} + +int32_t BrowsingContext::IndexOf(BrowsingContext* aChild) { + int32_t index = -1; + for (BrowsingContext* child : Children()) { + ++index; + if (child == aChild) { + break; + } + } + return index; +} + +WindowContext* BrowsingContext::GetTopWindowContext() const { + if (mParentWindow) { + return mParentWindow->TopWindowContext(); + } + return mCurrentWindowContext; +} + +/* static */ +void BrowsingContext::Init() { + if (!sBrowsingContexts) { + sBrowsingContexts = new BrowsingContextMap(); + sCurrentTopByBrowserId = new BrowsingContextMap(); + ClearOnShutdown(&sBrowsingContexts); + ClearOnShutdown(&sCurrentTopByBrowserId); + } +} + +/* static */ +LogModule* BrowsingContext::GetLog() { return gBrowsingContextLog; } + +/* static */ +LogModule* BrowsingContext::GetSyncLog() { return gBrowsingContextSyncLog; } + +/* static */ +already_AddRefed<BrowsingContext> BrowsingContext::Get(uint64_t aId) { + return do_AddRef(sBrowsingContexts->Get(aId)); +} + +/* static */ +already_AddRefed<BrowsingContext> BrowsingContext::GetCurrentTopByBrowserId( + uint64_t aBrowserId) { + return do_AddRef(sCurrentTopByBrowserId->Get(aBrowserId)); +} + +/* static */ +already_AddRefed<BrowsingContext> BrowsingContext::GetFromWindow( + WindowProxyHolder& aProxy) { + return do_AddRef(aProxy.get()); +} + +CanonicalBrowsingContext* BrowsingContext::Canonical() { + return CanonicalBrowsingContext::Cast(this); +} + +bool BrowsingContext::IsOwnedByProcess() const { + return mIsInProcess && mDocShell && + !nsDocShell::Cast(mDocShell)->WillChangeProcess(); +} + +bool BrowsingContext::SameOriginWithTop() { + MOZ_ASSERT(IsInProcess()); + // If the top BrowsingContext is not same-process to us, it is cross-origin + if (!Top()->IsInProcess()) { + return false; + } + + nsIDocShell* docShell = GetDocShell(); + if (!docShell) { + return false; + } + Document* doc = docShell->GetDocument(); + if (!doc) { + return false; + } + nsIPrincipal* principal = doc->NodePrincipal(); + + nsIDocShell* topDocShell = Top()->GetDocShell(); + if (!topDocShell) { + return false; + } + Document* topDoc = topDocShell->GetDocument(); + if (!topDoc) { + return false; + } + nsIPrincipal* topPrincipal = topDoc->NodePrincipal(); + + return principal->Equals(topPrincipal); +} + +/* static */ +already_AddRefed<BrowsingContext> BrowsingContext::CreateDetached( + nsGlobalWindowInner* aParent, BrowsingContext* aOpener, + BrowsingContextGroup* aSpecificGroup, const nsAString& aName, Type aType, + bool aIsPopupRequested, bool aCreatedDynamically) { + if (aParent) { + MOZ_DIAGNOSTIC_ASSERT(aParent->GetWindowContext()); + MOZ_DIAGNOSTIC_ASSERT(aParent->GetBrowsingContext()->mType == aType); + MOZ_DIAGNOSTIC_ASSERT(aParent->GetBrowsingContext()->GetBrowserId() != 0); + } + + MOZ_DIAGNOSTIC_ASSERT(aType != Type::Chrome || XRE_IsParentProcess()); + + uint64_t id = nsContentUtils::GenerateBrowsingContextId(); + + MOZ_LOG(GetLog(), LogLevel::Debug, + ("Creating 0x%08" PRIx64 " in %s", id, + XRE_IsParentProcess() ? "Parent" : "Child")); + + RefPtr<BrowsingContext> parentBC = + aParent ? aParent->GetBrowsingContext() : nullptr; + RefPtr<WindowContext> parentWC = + aParent ? aParent->GetWindowContext() : nullptr; + BrowsingContext* inherit = parentBC ? parentBC.get() : aOpener; + + // Determine which BrowsingContextGroup this context should be created in. + RefPtr<BrowsingContextGroup> group = aSpecificGroup; + if (aType == Type::Chrome) { + MOZ_DIAGNOSTIC_ASSERT(!group); + group = BrowsingContextGroup::GetChromeGroup(); + } else if (!group) { + group = BrowsingContextGroup::Select(parentWC, aOpener); + } + + // Configure initial values for synced fields. + FieldValues fields; + fields.Get<IDX_Name>() = aName; + + if (aOpener) { + MOZ_DIAGNOSTIC_ASSERT(!aParent, + "new BC with both initial opener and parent"); + MOZ_DIAGNOSTIC_ASSERT(aOpener->Group() == group); + MOZ_DIAGNOSTIC_ASSERT(aOpener->mType == aType); + fields.Get<IDX_OpenerId>() = aOpener->Id(); + fields.Get<IDX_HadOriginalOpener>() = true; + } + + if (aParent) { + MOZ_DIAGNOSTIC_ASSERT(parentBC->Group() == group); + MOZ_DIAGNOSTIC_ASSERT(parentBC->mType == aType); + fields.Get<IDX_EmbedderInnerWindowId>() = aParent->WindowID(); + // Non-toplevel content documents are always embededed within content. + fields.Get<IDX_EmbeddedInContentDocument>() = + parentBC->mType == Type::Content; + + // XXX(farre): Can/Should we check aParent->IsLoading() here? (Bug + // 1608448) Check if the parent was itself loading already + auto readystate = aParent->GetDocument()->GetReadyStateEnum(); + fields.Get<IDX_AncestorLoading>() = + parentBC->GetAncestorLoading() || + readystate == Document::ReadyState::READYSTATE_LOADING || + readystate == Document::ReadyState::READYSTATE_INTERACTIVE; + } + + fields.Get<IDX_BrowserId>() = + parentBC ? parentBC->GetBrowserId() : nsContentUtils::GenerateBrowserId(); + + fields.Get<IDX_OpenerPolicy>() = nsILoadInfo::OPENER_POLICY_UNSAFE_NONE; + if (aOpener && aOpener->SameOriginWithTop()) { + // We inherit the opener policy if there is a creator and if the creator's + // origin is same origin with the creator's top-level origin. + // If it is cross origin we should not inherit the CrossOriginOpenerPolicy + fields.Get<IDX_OpenerPolicy>() = aOpener->Top()->GetOpenerPolicy(); + + // If we inherit a policy which is potentially cross-origin isolated, we + // must be in a potentially cross-origin isolated BCG. + bool isPotentiallyCrossOriginIsolated = + fields.Get<IDX_OpenerPolicy>() == + nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP; + MOZ_RELEASE_ASSERT(isPotentiallyCrossOriginIsolated == + group->IsPotentiallyCrossOriginIsolated()); + } else if (aOpener) { + // They are not same origin + auto topPolicy = aOpener->Top()->GetOpenerPolicy(); + MOZ_RELEASE_ASSERT(topPolicy == nsILoadInfo::OPENER_POLICY_UNSAFE_NONE || + topPolicy == + nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_ALLOW_POPUPS); + } else if (!aParent && group->IsPotentiallyCrossOriginIsolated()) { + // If we're creating a brand-new toplevel BC in a potentially cross-origin + // isolated group, it should start out with a strict opener policy. + fields.Get<IDX_OpenerPolicy>() = + nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP; + } + + fields.Get<IDX_HistoryID>() = nsID::GenerateUUID(); + fields.Get<IDX_ExplicitActive>() = [&] { + if (parentBC) { + // Non-root browsing-contexts inherit their status from its parent. + return ExplicitActiveStatus::None; + } + if (aType == Type::Content) { + // Content gets managed by the chrome front-end / embedder element and + // starts as inactive. + return ExplicitActiveStatus::Inactive; + } + // Chrome starts as active. + return ExplicitActiveStatus::Active; + }(); + + fields.Get<IDX_FullZoom>() = parentBC ? parentBC->FullZoom() : 1.0f; + fields.Get<IDX_TextZoom>() = parentBC ? parentBC->TextZoom() : 1.0f; + + bool allowContentRetargeting = + inherit ? inherit->GetAllowContentRetargetingOnChildren() : true; + fields.Get<IDX_AllowContentRetargeting>() = allowContentRetargeting; + fields.Get<IDX_AllowContentRetargetingOnChildren>() = allowContentRetargeting; + + // Assume top allows fullscreen for its children unless otherwise stated. + // Subframes start with it false unless otherwise noted in SetEmbedderElement. + fields.Get<IDX_FullscreenAllowedByOwner>() = !aParent; + + fields.Get<IDX_AllowPlugins>() = inherit ? inherit->GetAllowPlugins() : true; + + fields.Get<IDX_DefaultLoadFlags>() = + inherit ? inherit->GetDefaultLoadFlags() : nsIRequest::LOAD_NORMAL; + + fields.Get<IDX_OrientationLock>() = mozilla::hal::ScreenOrientation::None; + + fields.Get<IDX_UseGlobalHistory>() = + inherit ? inherit->GetUseGlobalHistory() : false; + + fields.Get<IDX_UseErrorPages>() = true; + + fields.Get<IDX_TouchEventsOverrideInternal>() = TouchEventsOverride::None; + + fields.Get<IDX_AllowJavascript>() = + inherit ? inherit->GetAllowJavascript() : true; + + fields.Get<IDX_IsPopupRequested>() = aIsPopupRequested; + + if (!parentBC) { + fields.Get<IDX_ShouldDelayMediaFromStart>() = + StaticPrefs::media_block_autoplay_until_in_foreground(); + } + + RefPtr<BrowsingContext> context; + if (XRE_IsParentProcess()) { + context = new CanonicalBrowsingContext(parentWC, group, id, + /* aOwnerProcessId */ 0, + /* aEmbedderProcessId */ 0, aType, + std::move(fields)); + } else { + context = + new BrowsingContext(parentWC, group, id, aType, std::move(fields)); + } + + context->mEmbeddedByThisProcess = XRE_IsParentProcess() || aParent; + context->mCreatedDynamically = aCreatedDynamically; + if (inherit) { + context->mPrivateBrowsingId = inherit->mPrivateBrowsingId; + context->mUseRemoteTabs = inherit->mUseRemoteTabs; + context->mUseRemoteSubframes = inherit->mUseRemoteSubframes; + context->mOriginAttributes = inherit->mOriginAttributes; + } + + nsCOMPtr<nsIRequestContextService> rcsvc = + net::RequestContextService::GetOrCreate(); + if (rcsvc) { + nsCOMPtr<nsIRequestContext> requestContext; + nsresult rv = rcsvc->NewRequestContext(getter_AddRefs(requestContext)); + if (NS_SUCCEEDED(rv) && requestContext) { + context->mRequestContextId = requestContext->GetID(); + } + } + + return context.forget(); +} + +already_AddRefed<BrowsingContext> BrowsingContext::CreateIndependent( + Type aType) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(), + "BCs created in the content process must be related to " + "some BrowserChild"); + RefPtr<BrowsingContext> bc( + CreateDetached(nullptr, nullptr, nullptr, u""_ns, aType, false)); + bc->mWindowless = bc->IsContent(); + bc->mEmbeddedByThisProcess = true; + bc->EnsureAttached(); + return bc.forget(); +} + +void BrowsingContext::EnsureAttached() { + if (!mEverAttached) { + Register(this); + + // Attach the browsing context to the tree. + Attach(/* aFromIPC */ false, /* aOriginProcess */ nullptr); + } +} + +/* static */ +mozilla::ipc::IPCResult BrowsingContext::CreateFromIPC( + BrowsingContext::IPCInitializer&& aInit, BrowsingContextGroup* aGroup, + ContentParent* aOriginProcess) { + MOZ_DIAGNOSTIC_ASSERT(aOriginProcess || XRE_IsContentProcess()); + MOZ_DIAGNOSTIC_ASSERT(aGroup); + + uint64_t originId = 0; + if (aOriginProcess) { + originId = aOriginProcess->ChildID(); + aGroup->EnsureHostProcess(aOriginProcess); + } + + MOZ_LOG(GetLog(), LogLevel::Debug, + ("Creating 0x%08" PRIx64 " from IPC (origin=0x%08" PRIx64 ")", + aInit.mId, originId)); + + RefPtr<WindowContext> parent = aInit.GetParent(); + + RefPtr<BrowsingContext> context; + if (XRE_IsParentProcess()) { + // If the new BrowsingContext has a parent, it is a sub-frame embedded in + // whatever process sent the message. If it doesn't, and is not windowless, + // it is a new window or tab, and will be embedded in the parent process. + uint64_t embedderProcessId = (aInit.mWindowless || parent) ? originId : 0; + context = new CanonicalBrowsingContext(parent, aGroup, aInit.mId, originId, + embedderProcessId, Type::Content, + std::move(aInit.mFields)); + } else { + context = new BrowsingContext(parent, aGroup, aInit.mId, Type::Content, + std::move(aInit.mFields)); + } + + context->mWindowless = aInit.mWindowless; + context->mCreatedDynamically = aInit.mCreatedDynamically; + context->mChildOffset = aInit.mChildOffset; + if (context->GetHasSessionHistory()) { + context->CreateChildSHistory(); + if (mozilla::SessionHistoryInParent()) { + context->GetChildSessionHistory()->SetIndexAndLength( + aInit.mSessionHistoryIndex, aInit.mSessionHistoryCount, nsID()); + } + } + + // NOTE: Call through the `Set` methods for these values to ensure that any + // relevant process-local state is also updated. + context->SetOriginAttributes(aInit.mOriginAttributes); + context->SetRemoteTabs(aInit.mUseRemoteTabs); + context->SetRemoteSubframes(aInit.mUseRemoteSubframes); + context->mRequestContextId = aInit.mRequestContextId; + // NOTE: Private browsing ID is set by `SetOriginAttributes`. + + Register(context); + + return context->Attach(/* aFromIPC */ true, aOriginProcess); +} + +BrowsingContext::BrowsingContext(WindowContext* aParentWindow, + BrowsingContextGroup* aGroup, + uint64_t aBrowsingContextId, Type aType, + FieldValues&& aInit) + : mFields(std::move(aInit)), + mType(aType), + mBrowsingContextId(aBrowsingContextId), + mGroup(aGroup), + mParentWindow(aParentWindow), + mPrivateBrowsingId(0), + mEverAttached(false), + mIsInProcess(false), + mIsDiscarded(false), + mWindowless(false), + mDanglingRemoteOuterProxies(false), + mEmbeddedByThisProcess(false), + mUseRemoteTabs(false), + mUseRemoteSubframes(false), + mCreatedDynamically(false), + mIsInBFCache(false), + mCanExecuteScripts(true), + mChildOffset(0) { + MOZ_RELEASE_ASSERT(!mParentWindow || mParentWindow->Group() == mGroup); + MOZ_RELEASE_ASSERT(mBrowsingContextId != 0); + MOZ_RELEASE_ASSERT(mGroup); +} + +void BrowsingContext::SetDocShell(nsIDocShell* aDocShell) { + // XXX(nika): We should communicate that we are now an active BrowsingContext + // process to the parent & do other validation here. + MOZ_DIAGNOSTIC_ASSERT(mEverAttached); + MOZ_RELEASE_ASSERT(aDocShell->GetBrowsingContext() == this); + mDocShell = aDocShell; + mDanglingRemoteOuterProxies = !mIsInProcess; + mIsInProcess = true; + if (mChildSessionHistory) { + mChildSessionHistory->SetIsInProcess(true); + } + + RecomputeCanExecuteScripts(); +} + +// This class implements a callback that will return the remote window proxy for +// mBrowsingContext in that compartment, if it has one. It also removes the +// proxy from the map, because the object will be transplanted into another kind +// of object. +class MOZ_STACK_CLASS CompartmentRemoteProxyTransplantCallback + : public js::CompartmentTransplantCallback { + public: + explicit CompartmentRemoteProxyTransplantCallback( + BrowsingContext* aBrowsingContext) + : mBrowsingContext(aBrowsingContext) {} + + virtual JSObject* getObjectToTransplant( + JS::Compartment* compartment) override { + auto* priv = xpc::CompartmentPrivate::Get(compartment); + if (!priv) { + return nullptr; + } + + auto& map = priv->GetRemoteProxyMap(); + auto result = map.lookup(mBrowsingContext); + if (!result) { + return nullptr; + } + JSObject* resultObject = result->value(); + map.remove(result); + + return resultObject; + } + + private: + BrowsingContext* mBrowsingContext; +}; + +void BrowsingContext::CleanUpDanglingRemoteOuterWindowProxies( + JSContext* aCx, JS::MutableHandle<JSObject*> aOuter) { + if (!mDanglingRemoteOuterProxies) { + return; + } + mDanglingRemoteOuterProxies = false; + + CompartmentRemoteProxyTransplantCallback cb(this); + js::RemapRemoteWindowProxies(aCx, &cb, aOuter); +} + +bool BrowsingContext::IsActive() const { + const BrowsingContext* current = this; + do { + auto explicit_ = current->GetExplicitActive(); + if (explicit_ != ExplicitActiveStatus::None) { + return explicit_ == ExplicitActiveStatus::Active; + } + if (mParentWindow && !mParentWindow->IsCurrent()) { + return false; + } + } while ((current = current->GetParent())); + + return false; +} + +bool BrowsingContext::GetIsActiveBrowserWindow() { + if (!XRE_IsParentProcess()) { + return Top()->GetIsActiveBrowserWindowInternal(); + } + + // chrome:// urls loaded in the parent won't receive + // their own activation so we defer to the top chrome + // Browsing Context when in the parent process. + RefPtr<CanonicalBrowsingContext> chromeTop = + Canonical()->TopCrossChromeBoundary(); + return chromeTop->GetIsActiveBrowserWindowInternal(); +} + +void BrowsingContext::SetIsActiveBrowserWindow(bool aActive) { + Unused << SetIsActiveBrowserWindowInternal(aActive); +} + +bool BrowsingContext::FullscreenAllowed() const { + for (auto* current = this; current; current = current->GetParent()) { + if (!current->GetFullscreenAllowedByOwner()) { + return false; + } + } + return true; +} + +static bool OwnerAllowsFullscreen(const Element& aEmbedder) { + if (aEmbedder.IsXULElement()) { + return !aEmbedder.HasAttr(nsGkAtoms::disablefullscreen); + } + if (aEmbedder.IsHTMLElement(nsGkAtoms::iframe)) { + // This is controlled by feature policy. + return true; + } + if (const auto* embed = HTMLEmbedElement::FromNode(aEmbedder)) { + return embed->AllowFullscreen(); + } + return false; +} + +void BrowsingContext::SetEmbedderElement(Element* aEmbedder) { + mEmbeddedByThisProcess = true; + + // Update embedder-element-specific fields in a shared transaction. + // Don't do this when clearing our embedder, as we're being destroyed either + // way. + if (aEmbedder) { + Transaction txn; + txn.SetEmbedderElementType(Some(aEmbedder->LocalName())); + txn.SetEmbeddedInContentDocument( + aEmbedder->OwnerDoc()->IsContentDocument()); + if (nsCOMPtr<nsPIDOMWindowInner> inner = + do_QueryInterface(aEmbedder->GetOwnerGlobal())) { + txn.SetEmbedderInnerWindowId(inner->WindowID()); + } + txn.SetFullscreenAllowedByOwner(OwnerAllowsFullscreen(*aEmbedder)); + if (XRE_IsParentProcess() && IsTopContent()) { + nsAutoString messageManagerGroup; + if (aEmbedder->IsXULElement()) { + aEmbedder->GetAttr(nsGkAtoms::messagemanagergroup, messageManagerGroup); + if (!aEmbedder->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::initiallyactive, + nsGkAtoms::_false, eIgnoreCase)) { + txn.SetExplicitActive(ExplicitActiveStatus::Active); + } + } + txn.SetMessageManagerGroup(messageManagerGroup); + + bool useGlobalHistory = + !aEmbedder->HasAttr(nsGkAtoms::disableglobalhistory); + txn.SetUseGlobalHistory(useGlobalHistory); + } + + MOZ_ALWAYS_SUCCEEDS(txn.Commit(this)); + } + + if (XRE_IsParentProcess() && IsTopContent()) { + Canonical()->MaybeSetPermanentKey(aEmbedder); + } + + mEmbedderElement = aEmbedder; + + if (mEmbedderElement) { + if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) { + obs->NotifyWhenScriptSafe(ToSupports(this), + "browsing-context-did-set-embedder", nullptr); + } + + if (nsContentUtils::ShouldHideObjectOrEmbedImageDocument() && + IsEmbedderTypeObjectOrEmbed()) { + Unused << SetSyntheticDocumentContainer(true); + } + } +} + +bool BrowsingContext::IsEmbedderTypeObjectOrEmbed() { + if (const Maybe<nsString>& type = GetEmbedderElementType()) { + return nsGkAtoms::object->Equals(*type) || nsGkAtoms::embed->Equals(*type); + } + return false; +} + +void BrowsingContext::Embed() { + if (auto* frame = HTMLIFrameElement::FromNode(mEmbedderElement)) { + frame->BindToBrowsingContext(this); + } +} + +mozilla::ipc::IPCResult BrowsingContext::Attach(bool aFromIPC, + ContentParent* aOriginProcess) { + MOZ_DIAGNOSTIC_ASSERT(!mEverAttached); + MOZ_DIAGNOSTIC_ASSERT_IF(aFromIPC, aOriginProcess || XRE_IsContentProcess()); + mEverAttached = true; + + if (MOZ_LOG_TEST(GetLog(), LogLevel::Debug)) { + nsAutoCString suffix; + mOriginAttributes.CreateSuffix(suffix); + MOZ_LOG(GetLog(), LogLevel::Debug, + ("%s: Connecting 0x%08" PRIx64 " to 0x%08" PRIx64 + " (private=%d, remote=%d, fission=%d, oa=%s)", + XRE_IsParentProcess() ? "Parent" : "Child", Id(), + GetParent() ? GetParent()->Id() : 0, (int)mPrivateBrowsingId, + (int)mUseRemoteTabs, (int)mUseRemoteSubframes, suffix.get())); + } + + MOZ_DIAGNOSTIC_ASSERT(mGroup); + MOZ_DIAGNOSTIC_ASSERT(!mIsDiscarded); + + if (mGroup->IsPotentiallyCrossOriginIsolated() != + (Top()->GetOpenerPolicy() == + nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP)) { + MOZ_DIAGNOSTIC_ASSERT(aFromIPC); + if (aFromIPC) { + auto* actor = aOriginProcess + ? static_cast<mozilla::ipc::IProtocol*>(aOriginProcess) + : static_cast<mozilla::ipc::IProtocol*>( + ContentChild::GetSingleton()); + return IPC_FAIL( + actor, + "Invalid CrossOriginIsolated state in BrowsingContext::Attach call"); + } else { + MOZ_CRASH( + "Invalid CrossOriginIsolated state in BrowsingContext::Attach call"); + } + } + + AssertCoherentLoadContext(); + + // Add ourselves either to our parent or BrowsingContextGroup's child list. + // Important: We shouldn't return IPC_FAIL after this point, since the + // BrowsingContext will have already been added to the tree. + if (mParentWindow) { + if (!aFromIPC) { + MOZ_DIAGNOSTIC_ASSERT(!mParentWindow->IsDiscarded(), + "local attach in discarded window"); + MOZ_DIAGNOSTIC_ASSERT(!GetParent()->IsDiscarded(), + "local attach call in discarded bc"); + MOZ_DIAGNOSTIC_ASSERT(mParentWindow->GetWindowGlobalChild(), + "local attach call with oop parent window"); + MOZ_DIAGNOSTIC_ASSERT(mParentWindow->GetWindowGlobalChild()->CanSend(), + "local attach call with dead parent window"); + } + mChildOffset = + mCreatedDynamically ? -1 : mParentWindow->Children().Length(); + mParentWindow->AppendChildBrowsingContext(this); + RecomputeCanExecuteScripts(); + } else { + mGroup->Toplevels().AppendElement(this); + } + + if (GetIsPopupSpam()) { + PopupBlocker::RegisterOpenPopupSpam(); + } + + if (IsTop() && GetHasSessionHistory() && !mChildSessionHistory) { + CreateChildSHistory(); + } + + // Why the context is being attached. This will always be "attach" in the + // content process, but may be "replace" if it's known the context being + // replaced in the parent process. + const char16_t* why = u"attach"; + + if (XRE_IsContentProcess() && !aFromIPC) { + // Send attach to our parent if we need to. + ContentChild::GetSingleton()->SendCreateBrowsingContext( + mGroup->Id(), GetIPCInitializer()); + } else if (XRE_IsParentProcess()) { + // If this window was created as a subframe by a content process, it must be + // being hosted within the same BrowserParent as its mParentWindow. + // Toplevel BrowsingContexts created by content have their BrowserParent + // configured during `RecvConstructPopupBrowser`. + if (mParentWindow && aOriginProcess) { + MOZ_DIAGNOSTIC_ASSERT( + mParentWindow->Canonical()->GetContentParent() == aOriginProcess, + "Creator process isn't the same as our embedder?"); + Canonical()->SetCurrentBrowserParent( + mParentWindow->Canonical()->GetBrowserParent()); + } + + mGroup->EachOtherParent(aOriginProcess, [&](ContentParent* aParent) { + MOZ_DIAGNOSTIC_ASSERT(IsContent(), + "chrome BCG cannot be synced to content process"); + if (!Canonical()->IsEmbeddedInProcess(aParent->ChildID())) { + Unused << aParent->SendCreateBrowsingContext(mGroup->Id(), + GetIPCInitializer()); + } + }); + + if (IsTop() && IsContent() && Canonical()->GetWebProgress()) { + why = u"replace"; + } + + // We want to create a BrowsingContextWebProgress for all content + // BrowsingContexts. + if (IsContent() && !Canonical()->mWebProgress) { + Canonical()->mWebProgress = new BrowsingContextWebProgress(Canonical()); + } + } + + if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) { + obs->NotifyWhenScriptSafe(ToSupports(this), "browsing-context-attached", + why); + } + + if (XRE_IsParentProcess()) { + Canonical()->CanonicalAttach(); + } + return IPC_OK(); +} + +void BrowsingContext::Detach(bool aFromIPC) { + MOZ_LOG(GetLog(), LogLevel::Debug, + ("%s: Detaching 0x%08" PRIx64 " from 0x%08" PRIx64, + XRE_IsParentProcess() ? "Parent" : "Child", Id(), + GetParent() ? GetParent()->Id() : 0)); + + MOZ_DIAGNOSTIC_ASSERT(mEverAttached); + MOZ_DIAGNOSTIC_ASSERT(!mIsDiscarded); + + if (XRE_IsParentProcess()) { + Canonical()->AddPendingDiscard(); + } + auto callListeners = + MakeScopeExit([&, listeners = std::move(mDiscardListeners), id = Id()] { + for (const auto& listener : listeners) { + listener(id); + } + if (XRE_IsParentProcess()) { + Canonical()->RemovePendingDiscard(); + } + }); + + nsCOMPtr<nsIRequestContextService> rcsvc = + net::RequestContextService::GetOrCreate(); + if (rcsvc) { + rcsvc->RemoveRequestContext(GetRequestContextId()); + } + + // This will only ever be null if the cycle-collector has unlinked us. Don't + // try to detach ourselves in that case. + if (NS_WARN_IF(!mGroup)) { + MOZ_ASSERT_UNREACHABLE(); + return; + } + + if (mParentWindow) { + mParentWindow->RemoveChildBrowsingContext(this); + } else { + mGroup->Toplevels().RemoveElement(this); + } + + if (XRE_IsParentProcess()) { + RefPtr<CanonicalBrowsingContext> self{Canonical()}; + Group()->EachParent([&](ContentParent* aParent) { + // Only the embedder process is allowed to initiate a BrowsingContext + // detach, so if we've gotten here, the host process already knows we've + // been detached, and there's no need to tell it again. + // + // If the owner process is not the same as the embedder process, its + // BrowsingContext will be detached when its nsWebBrowser instance is + // destroyed. + bool doDiscard = !Canonical()->IsEmbeddedInProcess(aParent->ChildID()) && + !Canonical()->IsOwnedByProcess(aParent->ChildID()); + + // Hold a strong reference to ourself, and keep our BrowsingContextGroup + // alive, until the responses comes back to ensure we don't die while + // messages relating to this context are in-flight. + // + // When the callback is called, the keepalive on our group will be + // destroyed, and the reference to the BrowsingContext will be dropped, + // which may cause it to be fully destroyed. + mGroup->AddKeepAlive(); + self->AddPendingDiscard(); + auto callback = [self](auto) { + self->mGroup->RemoveKeepAlive(); + self->RemovePendingDiscard(); + }; + + aParent->SendDiscardBrowsingContext(this, doDiscard, callback, callback); + }); + } else { + // Hold a strong reference to ourself until the responses come back to + // ensure the BrowsingContext isn't cleaned up before the parent process + // acknowledges the discard request. + auto callback = [self = RefPtr{this}](auto) {}; + ContentChild::GetSingleton()->SendDiscardBrowsingContext( + this, !aFromIPC, callback, callback); + } + + mGroup->Unregister(this); + UnregisterBrowserId(this); + mIsDiscarded = true; + + if (XRE_IsParentProcess()) { + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm) { + fm->BrowsingContextDetached(this); + } + } + + if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) { + // Why the context is being discarded. This will always be "discard" in the + // content process, but may be "replace" if it's known the context being + // replaced in the parent process. + const char16_t* why = u"discard"; + if (XRE_IsParentProcess() && IsTop() && !Canonical()->GetWebProgress()) { + why = u"replace"; + } + obs->NotifyObservers(ToSupports(this), "browsing-context-discarded", why); + } + + // NOTE: Doesn't use SetClosed, as it will be set in all processes + // automatically by calls to Detach() + mFields.SetWithoutSyncing<IDX_Closed>(true); + + if (GetIsPopupSpam()) { + PopupBlocker::UnregisterOpenPopupSpam(); + // NOTE: Doesn't use SetIsPopupSpam, as it will be set all processes + // automatically. + mFields.SetWithoutSyncing<IDX_IsPopupSpam>(false); + } + + AssertOriginAttributesMatchPrivateBrowsing(); + + if (XRE_IsParentProcess()) { + Canonical()->CanonicalDiscard(); + } +} + +void BrowsingContext::AddDiscardListener( + std::function<void(uint64_t)>&& aListener) { + if (mIsDiscarded) { + aListener(Id()); + return; + } + mDiscardListeners.AppendElement(std::move(aListener)); +} + +void BrowsingContext::PrepareForProcessChange() { + MOZ_LOG(GetLog(), LogLevel::Debug, + ("%s: Preparing 0x%08" PRIx64 " for a process change", + XRE_IsParentProcess() ? "Parent" : "Child", Id())); + + MOZ_ASSERT(mIsInProcess, "Must currently be an in-process frame"); + MOZ_ASSERT(!mIsDiscarded, "We're already closed?"); + + mIsInProcess = false; + mUserGestureStart = TimeStamp(); + + // NOTE: For now, clear our nsDocShell reference, as we're primarily in a + // different process now. This may need to change in the future with + // Cross-Process BFCache. + mDocShell = nullptr; + if (mChildSessionHistory) { + // This can be removed once session history is stored exclusively in the + // parent process. + mChildSessionHistory->SetIsInProcess(false); + } + + if (!mWindowProxy) { + return; + } + + // We have to go through mWindowProxy rather than calling GetDOMWindow() on + // mDocShell because the mDocshell reference gets cleared immediately after + // the window is closed. + nsGlobalWindowOuter::PrepareForProcessChange(mWindowProxy); + MOZ_ASSERT(!mWindowProxy); +} + +bool BrowsingContext::IsTargetable() const { + return !GetClosed() && AncestorsAreCurrent(); +} + +bool BrowsingContext::HasOpener() const { + return sBrowsingContexts->Contains(GetOpenerId()); +} + +bool BrowsingContext::AncestorsAreCurrent() const { + const BrowsingContext* bc = this; + while (true) { + if (bc->IsDiscarded()) { + return false; + } + + if (WindowContext* wc = bc->GetParentWindowContext()) { + if (!wc->IsCurrent() || wc->IsDiscarded()) { + return false; + } + + bc = wc->GetBrowsingContext(); + } else { + return true; + } + } +} + +bool BrowsingContext::IsInBFCache() const { + if (mozilla::SessionHistoryInParent()) { + return mIsInBFCache; + } + return mParentWindow && + mParentWindow->TopWindowContext()->GetWindowStateSaved(); +} + +Span<RefPtr<BrowsingContext>> BrowsingContext::Children() const { + if (WindowContext* current = mCurrentWindowContext) { + return current->Children(); + } + return Span<RefPtr<BrowsingContext>>(); +} + +void BrowsingContext::GetChildren( + nsTArray<RefPtr<BrowsingContext>>& aChildren) { + aChildren.AppendElements(Children()); +} + +Span<RefPtr<BrowsingContext>> BrowsingContext::NonSyntheticChildren() const { + if (WindowContext* current = mCurrentWindowContext) { + return current->NonSyntheticChildren(); + } + return Span<RefPtr<BrowsingContext>>(); +} + +void BrowsingContext::GetWindowContexts( + nsTArray<RefPtr<WindowContext>>& aWindows) { + aWindows.AppendElements(mWindowContexts); +} + +void BrowsingContext::RegisterWindowContext(WindowContext* aWindow) { + MOZ_ASSERT(!mWindowContexts.Contains(aWindow), + "WindowContext already registered!"); + MOZ_ASSERT(aWindow->GetBrowsingContext() == this); + + mWindowContexts.AppendElement(aWindow); + + // If the newly registered WindowContext is for our current inner window ID, + // re-run the `DidSet` handler to re-establish the relationship. + if (aWindow->InnerWindowId() == GetCurrentInnerWindowId()) { + DidSet(FieldIndex<IDX_CurrentInnerWindowId>()); + MOZ_DIAGNOSTIC_ASSERT(mCurrentWindowContext == aWindow); + } +} + +void BrowsingContext::UnregisterWindowContext(WindowContext* aWindow) { + MOZ_ASSERT(mWindowContexts.Contains(aWindow), + "WindowContext not registered!"); + mWindowContexts.RemoveElement(aWindow); + + // If our currently active window was unregistered, clear our reference to it. + if (aWindow == mCurrentWindowContext) { + // Re-read our `CurrentInnerWindowId` value and use it to set + // `mCurrentWindowContext`. As `aWindow` is now unregistered and discarded, + // we won't find it, and the value will be cleared back to `nullptr`. + DidSet(FieldIndex<IDX_CurrentInnerWindowId>()); + MOZ_DIAGNOSTIC_ASSERT(mCurrentWindowContext == nullptr); + } +} + +void BrowsingContext::PreOrderWalkVoid( + const std::function<void(BrowsingContext*)>& aCallback) { + aCallback(this); + + AutoTArray<RefPtr<BrowsingContext>, 8> children; + children.AppendElements(Children()); + + for (auto& child : children) { + child->PreOrderWalkVoid(aCallback); + } +} + +BrowsingContext::WalkFlag BrowsingContext::PreOrderWalkFlag( + const std::function<WalkFlag(BrowsingContext*)>& aCallback) { + switch (aCallback(this)) { + case WalkFlag::Skip: + return WalkFlag::Next; + case WalkFlag::Stop: + return WalkFlag::Stop; + case WalkFlag::Next: + default: + break; + } + + AutoTArray<RefPtr<BrowsingContext>, 8> children; + children.AppendElements(Children()); + + for (auto& child : children) { + switch (child->PreOrderWalkFlag(aCallback)) { + case WalkFlag::Stop: + return WalkFlag::Stop; + default: + break; + } + } + + return WalkFlag::Next; +} + +void BrowsingContext::PostOrderWalk( + const std::function<void(BrowsingContext*)>& aCallback) { + AutoTArray<RefPtr<BrowsingContext>, 8> children; + children.AppendElements(Children()); + + for (auto& child : children) { + child->PostOrderWalk(aCallback); + } + + aCallback(this); +} + +void BrowsingContext::GetAllBrowsingContextsInSubtree( + nsTArray<RefPtr<BrowsingContext>>& aBrowsingContexts) { + PreOrderWalk([&](BrowsingContext* aContext) { + aBrowsingContexts.AppendElement(aContext); + }); +} + +BrowsingContext* BrowsingContext::FindChildWithName( + const nsAString& aName, WindowGlobalChild& aRequestingWindow) { + if (aName.IsEmpty()) { + // You can't find a browsing context with the empty name. + return nullptr; + } + + for (BrowsingContext* child : NonSyntheticChildren()) { + if (child->NameEquals(aName) && aRequestingWindow.CanNavigate(child) && + child->IsTargetable()) { + return child; + } + } + + return nullptr; +} + +BrowsingContext* BrowsingContext::FindWithSpecialName( + const nsAString& aName, WindowGlobalChild& aRequestingWindow) { + // TODO(farre): Neither BrowsingContext nor nsDocShell checks if the + // browsing context pointed to by a special name is active. Should + // it be? See Bug 1527913. + if (aName.LowerCaseEqualsLiteral("_self")) { + return this; + } + + if (aName.LowerCaseEqualsLiteral("_parent")) { + if (BrowsingContext* parent = GetParent()) { + return aRequestingWindow.CanNavigate(parent) ? parent : nullptr; + } + return this; + } + + if (aName.LowerCaseEqualsLiteral("_top")) { + BrowsingContext* top = Top(); + + return aRequestingWindow.CanNavigate(top) ? top : nullptr; + } + + return nullptr; +} + +BrowsingContext* BrowsingContext::FindWithNameInSubtree( + const nsAString& aName, WindowGlobalChild* aRequestingWindow) { + MOZ_DIAGNOSTIC_ASSERT(!aName.IsEmpty()); + + if (NameEquals(aName) && + (!aRequestingWindow || aRequestingWindow->CanNavigate(this)) && + IsTargetable()) { + return this; + } + + for (BrowsingContext* child : NonSyntheticChildren()) { + if (BrowsingContext* found = + child->FindWithNameInSubtree(aName, aRequestingWindow)) { + return found; + } + } + + return nullptr; +} + +bool BrowsingContext::IsSandboxedFrom(BrowsingContext* aTarget) { + // If no target then not sandboxed. + if (!aTarget) { + return false; + } + + // We cannot be sandboxed from ourselves. + if (aTarget == this) { + return false; + } + + // Default the sandbox flags to our flags, so that if we can't retrieve the + // active document, we will still enforce our own. + uint32_t sandboxFlags = GetSandboxFlags(); + if (mDocShell) { + if (RefPtr<Document> doc = mDocShell->GetExtantDocument()) { + sandboxFlags = doc->GetSandboxFlags(); + } + } + + // If no flags, we are not sandboxed at all. + if (!sandboxFlags) { + return false; + } + + // If aTarget has an ancestor, it is not top level. + if (RefPtr<BrowsingContext> ancestorOfTarget = aTarget->GetParent()) { + do { + // We are not sandboxed if we are an ancestor of target. + if (ancestorOfTarget == this) { + return false; + } + ancestorOfTarget = ancestorOfTarget->GetParent(); + } while (ancestorOfTarget); + + // Otherwise, we are sandboxed from aTarget. + return true; + } + + // aTarget is top level, are we the "one permitted sandboxed + // navigator", i.e. did we open aTarget? + if (aTarget->GetOnePermittedSandboxedNavigatorId() == Id()) { + return false; + } + + // If SANDBOXED_TOPLEVEL_NAVIGATION flag is not on, we are not sandboxed + // from our top. + if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION) && aTarget == Top()) { + return false; + } + + // If SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION flag is not on, we are not + // sandboxed from our top if we have user interaction. We assume there is a + // valid transient user gesture interaction if this check happens in the + // target process given that we have checked in the triggering process + // already. + if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION) && + mCurrentWindowContext && + (!mCurrentWindowContext->IsInProcess() || + mCurrentWindowContext->HasValidTransientUserGestureActivation()) && + aTarget == Top()) { + return false; + } + + // Otherwise, we are sandboxed from aTarget. + return true; +} + +RefPtr<SessionStorageManager> BrowsingContext::GetSessionStorageManager() { + RefPtr<SessionStorageManager>& manager = Top()->mSessionStorageManager; + if (!manager) { + manager = new SessionStorageManager(this); + } + return manager; +} + +bool BrowsingContext::CrossOriginIsolated() { + MOZ_ASSERT(NS_IsMainThread()); + + return StaticPrefs:: + dom_postMessage_sharedArrayBuffer_withCOOP_COEP_AtStartup() && + Top()->GetOpenerPolicy() == + nsILoadInfo:: + OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP && + XRE_IsContentProcess() && + StringBeginsWith(ContentChild::GetSingleton()->GetRemoteType(), + WITH_COOP_COEP_REMOTE_TYPE_PREFIX); +} + +void BrowsingContext::SetTriggeringAndInheritPrincipals( + nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit, + uint64_t aLoadIdentifier) { + mTriggeringPrincipal = Some( + PrincipalWithLoadIdentifierTuple(aTriggeringPrincipal, aLoadIdentifier)); + if (aPrincipalToInherit) { + mPrincipalToInherit = Some( + PrincipalWithLoadIdentifierTuple(aPrincipalToInherit, aLoadIdentifier)); + } +} + +std::tuple<nsCOMPtr<nsIPrincipal>, nsCOMPtr<nsIPrincipal>> +BrowsingContext::GetTriggeringAndInheritPrincipalsForCurrentLoad() { + nsCOMPtr<nsIPrincipal> triggeringPrincipal = + GetSavedPrincipal(mTriggeringPrincipal); + nsCOMPtr<nsIPrincipal> principalToInherit = + GetSavedPrincipal(mPrincipalToInherit); + return std::make_tuple(triggeringPrincipal, principalToInherit); +} + +nsIPrincipal* BrowsingContext::GetSavedPrincipal( + Maybe<PrincipalWithLoadIdentifierTuple> aPrincipalTuple) { + if (aPrincipalTuple) { + nsCOMPtr<nsIPrincipal> principal; + uint64_t loadIdentifier; + std::tie(principal, loadIdentifier) = *aPrincipalTuple; + // We want to return a principal only if the load identifier for it + // matches the current one for this BC. + if (auto current = GetCurrentLoadIdentifier(); + current && *current == loadIdentifier) { + return principal; + } + } + return nullptr; +} + +BrowsingContext::~BrowsingContext() { + MOZ_DIAGNOSTIC_ASSERT(!mParentWindow || + !mParentWindow->mChildren.Contains(this)); + MOZ_DIAGNOSTIC_ASSERT(!mGroup || !mGroup->Toplevels().Contains(this)); + + mDeprioritizedLoadRunner.clear(); + + if (sBrowsingContexts) { + sBrowsingContexts->Remove(Id()); + } + UnregisterBrowserId(this); +} + +/* static */ +void BrowsingContext::DiscardFromContentParent(ContentParent* aCP) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (sBrowsingContexts) { + AutoTArray<RefPtr<BrowsingContext>, 8> toDiscard; + for (const auto& data : sBrowsingContexts->Values()) { + auto* bc = data->Canonical(); + if (!bc->IsDiscarded() && bc->IsEmbeddedInProcess(aCP->ChildID())) { + toDiscard.AppendElement(bc); + } + } + + for (BrowsingContext* bc : toDiscard) { + bc->Detach(/* aFromIPC */ true); + } + } +} + +nsISupports* BrowsingContext::GetParentObject() const { + return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); +} + +JSObject* BrowsingContext::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return BrowsingContext_Binding::Wrap(aCx, this, aGivenProto); +} + +bool BrowsingContext::WriteStructuredClone(JSContext* aCx, + JSStructuredCloneWriter* aWriter, + StructuredCloneHolder* aHolder) { + MOZ_DIAGNOSTIC_ASSERT(mEverAttached); + return (JS_WriteUint32Pair(aWriter, SCTAG_DOM_BROWSING_CONTEXT, 0) && + JS_WriteUint32Pair(aWriter, uint32_t(Id()), uint32_t(Id() >> 32))); +} + +/* static */ +JSObject* BrowsingContext::ReadStructuredClone(JSContext* aCx, + JSStructuredCloneReader* aReader, + StructuredCloneHolder* aHolder) { + uint32_t idLow = 0; + uint32_t idHigh = 0; + if (!JS_ReadUint32Pair(aReader, &idLow, &idHigh)) { + return nullptr; + } + uint64_t id = uint64_t(idHigh) << 32 | idLow; + + // Note: Do this check after reading our ID data. Returning null will abort + // the decode operation anyway, but we should at least be as safe as possible. + if (NS_WARN_IF(!NS_IsMainThread())) { + MOZ_DIAGNOSTIC_ASSERT(false, + "We shouldn't be trying to decode a BrowsingContext " + "on a background thread."); + return nullptr; + } + + JS::Rooted<JS::Value> val(aCx, JS::NullValue()); + // We'll get rooting hazard errors from the RefPtr destructor if it isn't + // destroyed before we try to return a raw JSObject*, so create it in its own + // scope. + if (RefPtr<BrowsingContext> context = Get(id)) { + if (!GetOrCreateDOMReflector(aCx, context, &val) || !val.isObject()) { + return nullptr; + } + } + return val.toObjectOrNull(); +} + +bool BrowsingContext::CanSetOriginAttributes() { + // A discarded BrowsingContext has already been destroyed, and cannot modify + // its OriginAttributes. + if (NS_WARN_IF(IsDiscarded())) { + return false; + } + + // Before attaching is the safest time to set OriginAttributes, and the only + // allowed time for content BrowsingContexts. + if (!EverAttached()) { + return true; + } + + // Attached content BrowsingContexts may have been synced to other processes. + if (NS_WARN_IF(IsContent())) { + MOZ_CRASH(); + return false; + } + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + // Cannot set OriginAttributes after we've created our child BrowsingContext. + if (NS_WARN_IF(!Children().IsEmpty())) { + return false; + } + + // Only allow setting OriginAttributes if we have no associated document, or + // the document is still `about:blank`. + // TODO: Bug 1273058 - should have no document when setting origin attributes. + if (WindowGlobalParent* window = Canonical()->GetCurrentWindowGlobal()) { + if (nsIURI* uri = window->GetDocumentURI()) { + MOZ_ASSERT(NS_IsAboutBlank(uri)); + return NS_IsAboutBlank(uri); + } + } + return true; +} + +Nullable<WindowProxyHolder> BrowsingContext::GetAssociatedWindow() { + // nsILoadContext usually only returns same-process windows, + // so we intentionally return nullptr if this BC is out of + // process. + if (IsInProcess()) { + return WindowProxyHolder(this); + } + return nullptr; +} + +Nullable<WindowProxyHolder> BrowsingContext::GetTopWindow() { + return Top()->GetAssociatedWindow(); +} + +Element* BrowsingContext::GetTopFrameElement() { + return Top()->GetEmbedderElement(); +} + +void BrowsingContext::SetUsePrivateBrowsing(bool aUsePrivateBrowsing, + ErrorResult& aError) { + nsresult rv = SetUsePrivateBrowsing(aUsePrivateBrowsing); + if (NS_FAILED(rv)) { + aError.Throw(rv); + } +} + +void BrowsingContext::SetUseTrackingProtectionWebIDL( + bool aUseTrackingProtection, ErrorResult& aRv) { + SetForceEnableTrackingProtection(aUseTrackingProtection, aRv); +} + +void BrowsingContext::GetOriginAttributes(JSContext* aCx, + JS::MutableHandle<JS::Value> aVal, + ErrorResult& aError) { + AssertOriginAttributesMatchPrivateBrowsing(); + + if (!ToJSValue(aCx, mOriginAttributes, aVal)) { + aError.NoteJSContextException(aCx); + } +} + +NS_IMETHODIMP BrowsingContext::GetAssociatedWindow( + mozIDOMWindowProxy** aAssociatedWindow) { + nsCOMPtr<mozIDOMWindowProxy> win = GetDOMWindow(); + win.forget(aAssociatedWindow); + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::GetTopWindow(mozIDOMWindowProxy** aTopWindow) { + return Top()->GetAssociatedWindow(aTopWindow); +} + +NS_IMETHODIMP BrowsingContext::GetTopFrameElement(Element** aTopFrameElement) { + RefPtr<Element> topFrameElement = GetTopFrameElement(); + topFrameElement.forget(aTopFrameElement); + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::GetIsContent(bool* aIsContent) { + *aIsContent = IsContent(); + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::GetUsePrivateBrowsing( + bool* aUsePrivateBrowsing) { + *aUsePrivateBrowsing = mPrivateBrowsingId > 0; + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) { + if (!CanSetOriginAttributes()) { + bool changed = aUsePrivateBrowsing != (mPrivateBrowsingId > 0); + if (changed) { + NS_WARNING("SetUsePrivateBrowsing when !CanSetOriginAttributes()"); + } + return changed ? NS_ERROR_FAILURE : NS_OK; + } + + return SetPrivateBrowsing(aUsePrivateBrowsing); +} + +NS_IMETHODIMP BrowsingContext::SetPrivateBrowsing(bool aPrivateBrowsing) { + if (!CanSetOriginAttributes()) { + NS_WARNING("Attempt to set PrivateBrowsing when !CanSetOriginAttributes"); + return NS_ERROR_FAILURE; + } + + bool changed = aPrivateBrowsing != (mPrivateBrowsingId > 0); + if (changed) { + mPrivateBrowsingId = aPrivateBrowsing ? 1 : 0; + if (IsContent()) { + mOriginAttributes.SyncAttributesWithPrivateBrowsing(aPrivateBrowsing); + } + + if (XRE_IsParentProcess()) { + Canonical()->AdjustPrivateBrowsingCount(aPrivateBrowsing); + } + } + AssertOriginAttributesMatchPrivateBrowsing(); + + if (changed && mDocShell) { + nsDocShell::Cast(mDocShell)->NotifyPrivateBrowsingChanged(); + } + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::GetUseRemoteTabs(bool* aUseRemoteTabs) { + *aUseRemoteTabs = mUseRemoteTabs; + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::SetRemoteTabs(bool aUseRemoteTabs) { + if (!CanSetOriginAttributes()) { + NS_WARNING("Attempt to set RemoteTabs when !CanSetOriginAttributes"); + return NS_ERROR_FAILURE; + } + + static bool annotated = false; + if (aUseRemoteTabs && !annotated) { + annotated = true; + CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::DOMIPCEnabled, + true); + } + + // Don't allow non-remote tabs with remote subframes. + if (NS_WARN_IF(!aUseRemoteTabs && mUseRemoteSubframes)) { + return NS_ERROR_UNEXPECTED; + } + + mUseRemoteTabs = aUseRemoteTabs; + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::GetUseRemoteSubframes( + bool* aUseRemoteSubframes) { + *aUseRemoteSubframes = mUseRemoteSubframes; + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::SetRemoteSubframes(bool aUseRemoteSubframes) { + if (!CanSetOriginAttributes()) { + NS_WARNING("Attempt to set RemoteSubframes when !CanSetOriginAttributes"); + return NS_ERROR_FAILURE; + } + + static bool annotated = false; + if (aUseRemoteSubframes && !annotated) { + annotated = true; + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::DOMFissionEnabled, true); + } + + // Don't allow non-remote tabs with remote subframes. + if (NS_WARN_IF(aUseRemoteSubframes && !mUseRemoteTabs)) { + return NS_ERROR_UNEXPECTED; + } + + mUseRemoteSubframes = aUseRemoteSubframes; + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::GetUseTrackingProtection( + bool* aUseTrackingProtection) { + *aUseTrackingProtection = false; + + if (GetForceEnableTrackingProtection() || + StaticPrefs::privacy_trackingprotection_enabled() || + (UsePrivateBrowsing() && + StaticPrefs::privacy_trackingprotection_pbmode_enabled())) { + *aUseTrackingProtection = true; + return NS_OK; + } + + if (GetParent()) { + return GetParent()->GetUseTrackingProtection(aUseTrackingProtection); + } + + return NS_OK; +} + +NS_IMETHODIMP BrowsingContext::SetUseTrackingProtection( + bool aUseTrackingProtection) { + return SetForceEnableTrackingProtection(aUseTrackingProtection); +} + +NS_IMETHODIMP BrowsingContext::GetScriptableOriginAttributes( + JSContext* aCx, JS::MutableHandle<JS::Value> aVal) { + AssertOriginAttributesMatchPrivateBrowsing(); + + bool ok = ToJSValue(aCx, mOriginAttributes, aVal); + NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE); + return NS_OK; +} + +NS_IMETHODIMP_(void) +BrowsingContext::GetOriginAttributes(OriginAttributes& aAttrs) { + aAttrs = mOriginAttributes; + AssertOriginAttributesMatchPrivateBrowsing(); +} + +nsresult BrowsingContext::SetOriginAttributes(const OriginAttributes& aAttrs) { + if (!CanSetOriginAttributes()) { + NS_WARNING("Attempt to set OriginAttributes when !CanSetOriginAttributes"); + return NS_ERROR_FAILURE; + } + + AssertOriginAttributesMatchPrivateBrowsing(); + mOriginAttributes = aAttrs; + + bool isPrivate = mOriginAttributes.mPrivateBrowsingId != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID; + // Chrome Browsing Context can not contain OriginAttributes.mPrivateBrowsingId + if (IsChrome() && isPrivate) { + mOriginAttributes.mPrivateBrowsingId = + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID; + } + SetPrivateBrowsing(isPrivate); + AssertOriginAttributesMatchPrivateBrowsing(); + + return NS_OK; +} + +void BrowsingContext::AssertCoherentLoadContext() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + // LoadContext should generally match our opener or parent. + if (IsContent()) { + if (RefPtr<BrowsingContext> opener = GetOpener()) { + MOZ_DIAGNOSTIC_ASSERT(opener->mType == mType); + MOZ_DIAGNOSTIC_ASSERT(opener->mGroup == mGroup); + MOZ_DIAGNOSTIC_ASSERT(opener->mUseRemoteTabs == mUseRemoteTabs); + MOZ_DIAGNOSTIC_ASSERT(opener->mUseRemoteSubframes == mUseRemoteSubframes); + MOZ_DIAGNOSTIC_ASSERT(opener->mPrivateBrowsingId == mPrivateBrowsingId); + MOZ_DIAGNOSTIC_ASSERT( + opener->mOriginAttributes.EqualsIgnoringFPD(mOriginAttributes)); + } + } + if (RefPtr<BrowsingContext> parent = GetParent()) { + MOZ_DIAGNOSTIC_ASSERT(parent->mType == mType); + MOZ_DIAGNOSTIC_ASSERT(parent->mGroup == mGroup); + MOZ_DIAGNOSTIC_ASSERT(parent->mUseRemoteTabs == mUseRemoteTabs); + MOZ_DIAGNOSTIC_ASSERT(parent->mUseRemoteSubframes == mUseRemoteSubframes); + MOZ_DIAGNOSTIC_ASSERT(parent->mPrivateBrowsingId == mPrivateBrowsingId); + MOZ_DIAGNOSTIC_ASSERT( + parent->mOriginAttributes.EqualsIgnoringFPD(mOriginAttributes)); + } + + // UseRemoteSubframes and UseRemoteTabs must match. + MOZ_DIAGNOSTIC_ASSERT( + !mUseRemoteSubframes || mUseRemoteTabs, + "Cannot set useRemoteSubframes without also setting useRemoteTabs"); + + // Double-check OriginAttributes/Private Browsing + AssertOriginAttributesMatchPrivateBrowsing(); +#endif +} + +void BrowsingContext::AssertOriginAttributesMatchPrivateBrowsing() { + // Chrome browsing contexts must not have a private browsing OriginAttribute + // Content browsing contexts must maintain the equality: + // mOriginAttributes.mPrivateBrowsingId == mPrivateBrowsingId + if (IsChrome()) { + MOZ_DIAGNOSTIC_ASSERT(mOriginAttributes.mPrivateBrowsingId == 0); + } else { + MOZ_DIAGNOSTIC_ASSERT(mOriginAttributes.mPrivateBrowsingId == + mPrivateBrowsingId); + } +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BrowsingContext) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsILoadContext) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(BrowsingContext) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(BrowsingContext) +NS_IMPL_CYCLE_COLLECTING_RELEASE(BrowsingContext) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(BrowsingContext) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(BrowsingContext) + if (sBrowsingContexts) { + sBrowsingContexts->Remove(tmp->Id()); + } + UnregisterBrowserId(tmp); + + if (tmp->GetIsPopupSpam()) { + PopupBlocker::UnregisterOpenPopupSpam(); + // NOTE: Doesn't use SetIsPopupSpam, as it will be set all processes + // automatically. + tmp->mFields.SetWithoutSyncing<IDX_IsPopupSpam>(false); + } + + NS_IMPL_CYCLE_COLLECTION_UNLINK( + mDocShell, mParentWindow, mGroup, mEmbedderElement, mWindowContexts, + mCurrentWindowContext, mSessionStorageManager, mChildSessionHistory) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(BrowsingContext) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE( + mDocShell, mParentWindow, mGroup, mEmbedderElement, mWindowContexts, + mCurrentWindowContext, mSessionStorageManager, mChildSessionHistory) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +static bool IsCertainlyAliveForCC(BrowsingContext* aContext) { + return aContext->HasKnownLiveWrapper() || + (AppShutdown::GetCurrentShutdownPhase() == + ShutdownPhase::NotInShutdown && + aContext->EverAttached() && !aContext->IsDiscarded()); +} + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(BrowsingContext) + if (IsCertainlyAliveForCC(tmp)) { + if (tmp->PreservingWrapper()) { + tmp->MarkWrapperLive(); + } + return true; + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(BrowsingContext) + return IsCertainlyAliveForCC(tmp) && tmp->HasNothingToTrace(tmp); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(BrowsingContext) + return IsCertainlyAliveForCC(tmp); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END + +class RemoteLocationProxy + : public RemoteObjectProxy<BrowsingContext::LocationProxy, + Location_Binding::sCrossOriginProperties> { + public: + typedef RemoteObjectProxy Base; + + constexpr RemoteLocationProxy() + : RemoteObjectProxy(prototypes::id::Location) {} + + void NoteChildren(JSObject* aProxy, + nsCycleCollectionTraversalCallback& aCb) const override { + auto location = + static_cast<BrowsingContext::LocationProxy*>(GetNative(aProxy)); + CycleCollectionNoteChild(aCb, location->GetBrowsingContext(), + "JS::GetPrivate(obj)->GetBrowsingContext()"); + } +}; + +static const RemoteLocationProxy sSingleton; + +// Give RemoteLocationProxy 2 reserved slots, like the other wrappers, +// so JSObject::swap can swap it with CrossCompartmentWrappers without requiring +// malloc. +template <> +const JSClass RemoteLocationProxy::Base::sClass = + PROXY_CLASS_DEF("Proxy", JSCLASS_HAS_RESERVED_SLOTS(2)); + +void BrowsingContext::Location(JSContext* aCx, + JS::MutableHandle<JSObject*> aLocation, + ErrorResult& aError) { + aError.MightThrowJSException(); + sSingleton.GetProxyObject(aCx, &mLocation, /* aTransplantTo = */ nullptr, + aLocation); + if (!aLocation) { + aError.StealExceptionFromJSContext(aCx); + } +} + +bool BrowsingContext::RemoveRootFromBFCacheSync() { + if (WindowContext* wc = GetParentWindowContext()) { + if (RefPtr<Document> doc = wc->TopWindowContext()->GetDocument()) { + return doc->RemoveFromBFCacheSync(); + } + } + return false; +} + +nsresult BrowsingContext::CheckSandboxFlags(nsDocShellLoadState* aLoadState) { + const auto& sourceBC = aLoadState->SourceBrowsingContext(); + if (sourceBC.IsNull()) { + return NS_OK; + } + + // We might be called after the source BC has been discarded, but before we've + // destroyed our in-process instance of the BrowsingContext object in some + // situations (e.g. after creating a new pop-up with window.open while the + // window is being closed). In these situations we want to still perform the + // sandboxing check against our in-process copy. If we've forgotten about the + // context already, assume it is sanboxed. (bug 1643450) + BrowsingContext* bc = sourceBC.GetMaybeDiscarded(); + if (!bc || bc->IsSandboxedFrom(this)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + return NS_OK; +} + +nsresult BrowsingContext::LoadURI(nsDocShellLoadState* aLoadState, + bool aSetNavigating) { + // Per spec, most load attempts are silently ignored when a BrowsingContext is + // null (which in our code corresponds to discarded), so we simply fail + // silently in those cases. Regardless, we cannot trigger loads in/from + // discarded BrowsingContexts via IPC, so we need to abort in any case. + if (IsDiscarded()) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(aLoadState->TargetBrowsingContext().IsNull(), + "Targeting occurs in InternalLoad"); + + if (mDocShell) { + nsCOMPtr<nsIDocShell> docShell = mDocShell; + return docShell->LoadURI(aLoadState, aSetNavigating); + } + + // Note: We do this check both here and in `nsDocShell::InternalLoad`, since + // document-specific sandbox flags are only available in the process + // triggering the load, and we don't want the target process to have to trust + // the triggering process to do the appropriate checks for the + // BrowsingContext's sandbox flags. + MOZ_TRY(CheckSandboxFlags(aLoadState)); + SetTriggeringAndInheritPrincipals(aLoadState->TriggeringPrincipal(), + aLoadState->PrincipalToInherit(), + aLoadState->GetLoadIdentifier()); + + const auto& sourceBC = aLoadState->SourceBrowsingContext(); + + if (net::SchemeIsJavascript(aLoadState->URI())) { + if (!XRE_IsParentProcess()) { + // Web content should only be able to load javascript: URIs into documents + // whose principals the caller principal subsumes, which by definition + // excludes any document in a cross-process BrowsingContext. + return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI; + } + MOZ_DIAGNOSTIC_ASSERT(!sourceBC, + "Should never see a cross-process javascript: load " + "triggered from content"); + } + + MOZ_DIAGNOSTIC_ASSERT(!sourceBC || sourceBC->Group() == Group()); + if (sourceBC && sourceBC->IsInProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> win(sourceBC->GetDOMWindow()); + if (WindowGlobalChild* wgc = + win->GetCurrentInnerWindow()->GetWindowGlobalChild()) { + if (!wgc->CanNavigate(this)) { + return NS_ERROR_DOM_PROP_ACCESS_DENIED; + } + wgc->SendLoadURI(this, mozilla::WrapNotNull(aLoadState), aSetNavigating); + } + } else if (XRE_IsParentProcess()) { + if (Canonical()->LoadInParent(aLoadState, aSetNavigating)) { + return NS_OK; + } + + if (ContentParent* cp = Canonical()->GetContentParent()) { + // Attempt to initiate this load immediately in the parent, if it succeeds + // it'll return a unique identifier so that we can find it later. + uint64_t loadIdentifier = 0; + if (Canonical()->AttemptSpeculativeLoadInParent(aLoadState)) { + MOZ_DIAGNOSTIC_ASSERT(GetCurrentLoadIdentifier().isSome()); + loadIdentifier = GetCurrentLoadIdentifier().value(); + aLoadState->SetChannelInitialized(true); + } + + cp->TransmitBlobDataIfBlobURL(aLoadState->URI()); + + // Setup a confirmation callback once the content process receives this + // load. Normally we'd expect a PDocumentChannel actor to have been + // created to claim the load identifier by that time. If not, then it + // won't be coming, so make sure we clean up and deregister. + cp->SendLoadURI(this, mozilla::WrapNotNull(aLoadState), aSetNavigating) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [loadIdentifier]( + const PContentParent::LoadURIPromise::ResolveOrRejectValue& + aValue) { + if (loadIdentifier) { + net::DocumentLoadListener::CleanupParentLoadAttempt( + loadIdentifier); + } + }); + } + } else { + MOZ_DIAGNOSTIC_ASSERT(sourceBC); + if (!sourceBC) { + return NS_ERROR_UNEXPECTED; + } + // If we're in a content process and the source BC is no longer in-process, + // just fail silently. + } + return NS_OK; +} + +nsresult BrowsingContext::InternalLoad(nsDocShellLoadState* aLoadState) { + if (IsDiscarded()) { + return NS_OK; + } + SetTriggeringAndInheritPrincipals(aLoadState->TriggeringPrincipal(), + aLoadState->PrincipalToInherit(), + aLoadState->GetLoadIdentifier()); + + MOZ_DIAGNOSTIC_ASSERT(aLoadState->Target().IsEmpty(), + "should already have retargeted"); + MOZ_DIAGNOSTIC_ASSERT(!aLoadState->TargetBrowsingContext().IsNull(), + "should have target bc set"); + MOZ_DIAGNOSTIC_ASSERT(aLoadState->TargetBrowsingContext() == this, + "must be targeting this BrowsingContext"); + + if (mDocShell) { + RefPtr<nsDocShell> docShell = nsDocShell::Cast(mDocShell); + return docShell->InternalLoad(aLoadState); + } + + // Note: We do this check both here and in `nsDocShell::InternalLoad`, since + // document-specific sandbox flags are only available in the process + // triggering the load, and we don't want the target process to have to trust + // the triggering process to do the appropriate checks for the + // BrowsingContext's sandbox flags. + MOZ_TRY(CheckSandboxFlags(aLoadState)); + + const auto& sourceBC = aLoadState->SourceBrowsingContext(); + + if (net::SchemeIsJavascript(aLoadState->URI())) { + if (!XRE_IsParentProcess()) { + // Web content should only be able to load javascript: URIs into documents + // whose principals the caller principal subsumes, which by definition + // excludes any document in a cross-process BrowsingContext. + return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI; + } + MOZ_DIAGNOSTIC_ASSERT(!sourceBC, + "Should never see a cross-process javascript: load " + "triggered from content"); + } + + if (XRE_IsParentProcess()) { + ContentParent* cp = Canonical()->GetContentParent(); + if (!cp || !cp->CanSend()) { + return NS_ERROR_FAILURE; + } + + MOZ_ALWAYS_SUCCEEDS( + SetCurrentLoadIdentifier(Some(aLoadState->GetLoadIdentifier()))); + Unused << cp->SendInternalLoad(mozilla::WrapNotNull(aLoadState)); + } else { + MOZ_DIAGNOSTIC_ASSERT(sourceBC); + MOZ_DIAGNOSTIC_ASSERT(sourceBC->Group() == Group()); + + nsCOMPtr<nsPIDOMWindowOuter> win(sourceBC->GetDOMWindow()); + WindowGlobalChild* wgc = + win->GetCurrentInnerWindow()->GetWindowGlobalChild(); + if (!wgc || !wgc->CanSend()) { + return NS_ERROR_FAILURE; + } + if (!wgc->CanNavigate(this)) { + return NS_ERROR_DOM_PROP_ACCESS_DENIED; + } + + MOZ_ALWAYS_SUCCEEDS( + SetCurrentLoadIdentifier(Some(aLoadState->GetLoadIdentifier()))); + wgc->SendInternalLoad(mozilla::WrapNotNull(aLoadState)); + } + + return NS_OK; +} + +void BrowsingContext::DisplayLoadError(const nsAString& aURI) { + MOZ_LOG(GetLog(), LogLevel::Debug, ("DisplayLoadError")); + MOZ_DIAGNOSTIC_ASSERT(!IsDiscarded()); + MOZ_DIAGNOSTIC_ASSERT(mDocShell || XRE_IsParentProcess()); + + if (mDocShell) { + bool didDisplayLoadError = false; + nsCOMPtr<nsIDocShell> docShell = mDocShell; + docShell->DisplayLoadError(NS_ERROR_MALFORMED_URI, nullptr, + PromiseFlatString(aURI).get(), nullptr, + &didDisplayLoadError); + } else { + if (ContentParent* cp = Canonical()->GetContentParent()) { + Unused << cp->SendDisplayLoadError(this, PromiseFlatString(aURI)); + } + } +} + +WindowProxyHolder BrowsingContext::Window() { + return WindowProxyHolder(Self()); +} + +WindowProxyHolder BrowsingContext::GetFrames(ErrorResult& aError) { + return Window(); +} + +void BrowsingContext::Close(CallerType aCallerType, ErrorResult& aError) { + if (mIsDiscarded) { + return; + } + + if (IsSubframe()) { + // .close() on frames is a no-op. + return; + } + + if (GetDOMWindow()) { + nsGlobalWindowOuter::Cast(GetDOMWindow()) + ->CloseOuter(aCallerType == CallerType::System); + return; + } + + // This is a bit of a hack for webcompat. Content needs to see an updated + // |window.closed| value as early as possible, so we set this before we + // actually send the DOMWindowClose event, which happens in the process where + // the document for this browsing context is loaded. + MOZ_ALWAYS_SUCCEEDS(SetClosed(true)); + + if (ContentChild* cc = ContentChild::GetSingleton()) { + cc->SendWindowClose(this, aCallerType == CallerType::System); + } else if (ContentParent* cp = Canonical()->GetContentParent()) { + Unused << cp->SendWindowClose(this, aCallerType == CallerType::System); + } +} + +/* + * Examine the current document state to see if we're in a way that is + * typically abused by web designers. The window.open code uses this + * routine to determine whether to allow the new window. + * Returns a value from the PopupControlState enum. + */ +PopupBlocker::PopupControlState BrowsingContext::RevisePopupAbuseLevel( + PopupBlocker::PopupControlState aControl) { + if (!IsContent()) { + return PopupBlocker::openAllowed; + } + + RefPtr<Document> doc = GetExtantDocument(); + PopupBlocker::PopupControlState abuse = aControl; + switch (abuse) { + case PopupBlocker::openControlled: + case PopupBlocker::openBlocked: + case PopupBlocker::openOverridden: + if (IsPopupAllowed()) { + abuse = PopupBlocker::PopupControlState(abuse - 1); + } + break; + case PopupBlocker::openAbused: + if (IsPopupAllowed() || + (doc && doc->HasValidTransientUserGestureActivation())) { + // Skip PopupBlocker::openBlocked + abuse = PopupBlocker::openControlled; + } + break; + case PopupBlocker::openAllowed: + break; + default: + NS_WARNING("Strange PopupControlState!"); + } + + // limit the number of simultaneously open popups + if (abuse == PopupBlocker::openAbused || abuse == PopupBlocker::openBlocked || + abuse == PopupBlocker::openControlled) { + int32_t popupMax = StaticPrefs::dom_popup_maximum(); + if (popupMax >= 0 && + PopupBlocker::GetOpenPopupSpamCount() >= (uint32_t)popupMax) { + abuse = PopupBlocker::openOverridden; + } + } + + // If we're currently in-process, attempt to consume transient user gesture + // activations. + if (doc) { + // HACK: Some pages using bogus library + UA sniffing call window.open() + // from a blank iframe, only on Firefox, see bug 1685056. + // + // This is a hack-around to preserve behavior in that particular and + // specific case, by consuming activation on the parent document, so we + // don't care about the InProcessParent bits not being fission-safe or what + // not. + auto ConsumeTransientUserActivationForMultiplePopupBlocking = + [&]() -> bool { + if (doc->ConsumeTransientUserGestureActivation()) { + return true; + } + if (!doc->IsInitialDocument()) { + return false; + } + Document* parentDoc = doc->GetInProcessParentDocument(); + if (!parentDoc || + !parentDoc->NodePrincipal()->Equals(doc->NodePrincipal())) { + return false; + } + return parentDoc->ConsumeTransientUserGestureActivation(); + }; + + // If this popup is allowed, let's block any other for this event, forcing + // PopupBlocker::openBlocked state. + if ((abuse == PopupBlocker::openAllowed || + abuse == PopupBlocker::openControlled) && + StaticPrefs::dom_block_multiple_popups() && !IsPopupAllowed() && + !ConsumeTransientUserActivationForMultiplePopupBlocking()) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + doc, nsContentUtils::eDOM_PROPERTIES, + "MultiplePopupsBlockedNoUserActivation"); + abuse = PopupBlocker::openBlocked; + } + } + + return abuse; +} + +void BrowsingContext::IncrementHistoryEntryCountForBrowsingContext() { + Unused << SetHistoryEntryCount(GetHistoryEntryCount() + 1); +} + +std::tuple<bool, bool> BrowsingContext::CanFocusCheck(CallerType aCallerType) { + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (!fm) { + return {false, false}; + } + + nsCOMPtr<nsPIDOMWindowInner> caller = do_QueryInterface(GetEntryGlobal()); + BrowsingContext* callerBC = caller ? caller->GetBrowsingContext() : nullptr; + RefPtr<BrowsingContext> openerBC = GetOpener(); + MOZ_DIAGNOSTIC_ASSERT(!openerBC || openerBC->Group() == Group()); + + // Enforce dom.disable_window_flip (for non-chrome), but still allow the + // window which opened us to raise us at times when popups are allowed + // (bugs 355482 and 369306). + bool canFocus = aCallerType == CallerType::System || + !Preferences::GetBool("dom.disable_window_flip", true); + if (!canFocus && openerBC == callerBC) { + canFocus = + (callerBC ? callerBC : this) + ->RevisePopupAbuseLevel(PopupBlocker::GetPopupControlState()) < + PopupBlocker::openBlocked; + } + + bool isActive = false; + if (XRE_IsParentProcess()) { + RefPtr<CanonicalBrowsingContext> chromeTop = + Canonical()->TopCrossChromeBoundary(); + nsCOMPtr<nsPIDOMWindowOuter> activeWindow = fm->GetActiveWindow(); + isActive = (activeWindow == chromeTop->GetDOMWindow()); + } else { + isActive = (fm->GetActiveBrowsingContext() == Top()); + } + + return {canFocus, isActive}; +} + +void BrowsingContext::Focus(CallerType aCallerType, ErrorResult& aError) { + // These checks need to happen before the RequestFrameFocus call, which + // is why they are done in an untrusted process. If we wanted to enforce + // these in the parent, we'd need to do the checks there _also_. + // These should be kept in sync with nsGlobalWindowOuter::FocusOuter. + + auto [canFocus, isActive] = CanFocusCheck(aCallerType); + + if (!(canFocus || isActive)) { + return; + } + + // Permission check passed + + if (mEmbedderElement) { + // Make the activeElement in this process update synchronously. + nsContentUtils::RequestFrameFocus(*mEmbedderElement, true, aCallerType); + } + uint64_t actionId = nsFocusManager::GenerateFocusActionId(); + if (ContentChild* cc = ContentChild::GetSingleton()) { + cc->SendWindowFocus(this, aCallerType, actionId); + } else if (ContentParent* cp = Canonical()->GetContentParent()) { + Unused << cp->SendWindowFocus(this, aCallerType, actionId); + } +} + +bool BrowsingContext::CanBlurCheck(CallerType aCallerType) { + // If dom.disable_window_flip == true, then content should not be allowed + // to do blur (this would allow popunders, bug 369306) + return aCallerType == CallerType::System || + !Preferences::GetBool("dom.disable_window_flip", true); +} + +void BrowsingContext::Blur(CallerType aCallerType, ErrorResult& aError) { + if (!CanBlurCheck(aCallerType)) { + return; + } + + if (ContentChild* cc = ContentChild::GetSingleton()) { + cc->SendWindowBlur(this, aCallerType); + } else if (ContentParent* cp = Canonical()->GetContentParent()) { + Unused << cp->SendWindowBlur(this, aCallerType); + } +} + +Nullable<WindowProxyHolder> BrowsingContext::GetWindow() { + if (XRE_IsParentProcess() && !IsInProcess()) { + return nullptr; + } + return WindowProxyHolder(this); +} + +Nullable<WindowProxyHolder> BrowsingContext::GetTop(ErrorResult& aError) { + if (mIsDiscarded) { + return nullptr; + } + + // We never return null or throw an error, but the implementation in + // nsGlobalWindow does and we need to use the same signature. + return WindowProxyHolder(Top()); +} + +void BrowsingContext::GetOpener(JSContext* aCx, + JS::MutableHandle<JS::Value> aOpener, + ErrorResult& aError) const { + RefPtr<BrowsingContext> opener = GetOpener(); + if (!opener) { + aOpener.setNull(); + return; + } + + if (!ToJSValue(aCx, WindowProxyHolder(opener), aOpener)) { + aError.NoteJSContextException(aCx); + } +} + +// We never throw an error, but the implementation in nsGlobalWindow does and +// we need to use the same signature. +Nullable<WindowProxyHolder> BrowsingContext::GetParent(ErrorResult& aError) { + if (mIsDiscarded) { + return nullptr; + } + + if (GetParent()) { + return WindowProxyHolder(GetParent()); + } + return WindowProxyHolder(this); +} + +void BrowsingContext::PostMessageMoz(JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const nsAString& aTargetOrigin, + const Sequence<JSObject*>& aTransfer, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + if (mIsDiscarded) { + return; + } + + RefPtr<BrowsingContext> sourceBc; + PostMessageData data; + data.targetOrigin() = aTargetOrigin; + data.subjectPrincipal() = &aSubjectPrincipal; + RefPtr<nsGlobalWindowInner> callerInnerWindow; + nsAutoCString scriptLocation; + // We don't need to get the caller's agentClusterId since that is used for + // checking whether it's okay to sharing memory (and it's not allowed to share + // memory cross processes) + if (!nsGlobalWindowOuter::GatherPostMessageData( + aCx, aTargetOrigin, getter_AddRefs(sourceBc), data.origin(), + getter_AddRefs(data.targetOriginURI()), + getter_AddRefs(data.callerPrincipal()), + getter_AddRefs(callerInnerWindow), getter_AddRefs(data.callerURI()), + /* aCallerAgentClusterId */ nullptr, &scriptLocation, aError)) { + return; + } + if (sourceBc && sourceBc->IsDiscarded()) { + return; + } + data.source() = sourceBc; + data.isFromPrivateWindow() = + callerInnerWindow && + nsScriptErrorBase::ComputeIsFromPrivateWindow(callerInnerWindow); + data.innerWindowId() = callerInnerWindow ? callerInnerWindow->WindowID() : 0; + data.scriptLocation() = scriptLocation; + JS::Rooted<JS::Value> transferArray(aCx); + aError = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransfer, + &transferArray); + if (NS_WARN_IF(aError.Failed())) { + return; + } + + JS::CloneDataPolicy clonePolicy; + if (callerInnerWindow && callerInnerWindow->IsSharedMemoryAllowed()) { + clonePolicy.allowSharedMemoryObjects(); + } + + // We will see if the message is required to be in the same process or it can + // be in the different process after Write(). + ipc::StructuredCloneData message = ipc::StructuredCloneData( + StructuredCloneHolder::StructuredCloneScope::UnknownDestination, + StructuredCloneHolder::TransferringSupported); + message.Write(aCx, aMessage, transferArray, clonePolicy, aError); + if (NS_WARN_IF(aError.Failed())) { + return; + } + + ClonedOrErrorMessageData messageData; + if (ContentChild* cc = ContentChild::GetSingleton()) { + // The clone scope gets set when we write the message data based on the + // requirements of that data that we're writing. + // If the message data contains a shared memory object, then CloneScope + // would return SameProcess. Otherwise, it returns DifferentProcess. + if (message.CloneScope() == + StructuredCloneHolder::StructuredCloneScope::DifferentProcess) { + ClonedMessageData clonedMessageData; + if (!message.BuildClonedMessageData(clonedMessageData)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + messageData = std::move(clonedMessageData); + } else { + MOZ_ASSERT(message.CloneScope() == + StructuredCloneHolder::StructuredCloneScope::SameProcess); + + messageData = ErrorMessageData(); + + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "DOM Window"_ns, + callerInnerWindow ? callerInnerWindow->GetDocument() : nullptr, + nsContentUtils::eDOM_PROPERTIES, + "PostMessageSharedMemoryObjectToCrossOriginWarning"); + } + + cc->SendWindowPostMessage(this, messageData, data); + } else if (ContentParent* cp = Canonical()->GetContentParent()) { + if (message.CloneScope() == + StructuredCloneHolder::StructuredCloneScope::DifferentProcess) { + ClonedMessageData clonedMessageData; + if (!message.BuildClonedMessageData(clonedMessageData)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + messageData = std::move(clonedMessageData); + } else { + MOZ_ASSERT(message.CloneScope() == + StructuredCloneHolder::StructuredCloneScope::SameProcess); + + messageData = ErrorMessageData(); + + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "DOM Window"_ns, + callerInnerWindow ? callerInnerWindow->GetDocument() : nullptr, + nsContentUtils::eDOM_PROPERTIES, + "PostMessageSharedMemoryObjectToCrossOriginWarning"); + } + + Unused << cp->SendWindowPostMessage(this, messageData, data); + } +} + +void BrowsingContext::PostMessageMoz(JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const WindowPostMessageOptions& aOptions, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + PostMessageMoz(aCx, aMessage, aOptions.mTargetOrigin, aOptions.mTransfer, + aSubjectPrincipal, aError); +} + +void BrowsingContext::SendCommitTransaction(ContentParent* aParent, + const BaseTransaction& aTxn, + uint64_t aEpoch) { + Unused << aParent->SendCommitBrowsingContextTransaction(this, aTxn, aEpoch); +} + +void BrowsingContext::SendCommitTransaction(ContentChild* aChild, + const BaseTransaction& aTxn, + uint64_t aEpoch) { + aChild->SendCommitBrowsingContextTransaction(this, aTxn, aEpoch); +} + +BrowsingContext::IPCInitializer BrowsingContext::GetIPCInitializer() { + MOZ_DIAGNOSTIC_ASSERT(mEverAttached); + MOZ_DIAGNOSTIC_ASSERT(mType == Type::Content); + + IPCInitializer init; + init.mId = Id(); + init.mParentId = mParentWindow ? mParentWindow->Id() : 0; + init.mWindowless = mWindowless; + init.mUseRemoteTabs = mUseRemoteTabs; + init.mUseRemoteSubframes = mUseRemoteSubframes; + init.mCreatedDynamically = mCreatedDynamically; + init.mChildOffset = mChildOffset; + init.mOriginAttributes = mOriginAttributes; + if (mChildSessionHistory && mozilla::SessionHistoryInParent()) { + init.mSessionHistoryIndex = mChildSessionHistory->Index(); + init.mSessionHistoryCount = mChildSessionHistory->Count(); + } + init.mRequestContextId = mRequestContextId; + init.mFields = mFields.RawValues(); + return init; +} + +already_AddRefed<WindowContext> BrowsingContext::IPCInitializer::GetParent() { + RefPtr<WindowContext> parent; + if (mParentId != 0) { + parent = WindowContext::GetById(mParentId); + MOZ_RELEASE_ASSERT(parent); + } + return parent.forget(); +} + +already_AddRefed<BrowsingContext> BrowsingContext::IPCInitializer::GetOpener() { + RefPtr<BrowsingContext> opener; + if (GetOpenerId() != 0) { + opener = BrowsingContext::Get(GetOpenerId()); + MOZ_RELEASE_ASSERT(opener); + } + return opener.forget(); +} + +void BrowsingContext::StartDelayedAutoplayMediaComponents() { + if (!mDocShell) { + return; + } + AUTOPLAY_LOG("%s : StartDelayedAutoplayMediaComponents for bc 0x%08" PRIx64, + XRE_IsParentProcess() ? "Parent" : "Child", Id()); + mDocShell->StartDelayedAutoplayMediaComponents(); +} + +nsresult BrowsingContext::ResetGVAutoplayRequestStatus() { + MOZ_ASSERT(IsTop(), + "Should only set GVAudibleAutoplayRequestStatus in the top-level " + "browsing context"); + + Transaction txn; + txn.SetGVAudibleAutoplayRequestStatus(GVAutoplayRequestStatus::eUNKNOWN); + txn.SetGVInaudibleAutoplayRequestStatus(GVAutoplayRequestStatus::eUNKNOWN); + return txn.Commit(this); +} + +template <typename Callback> +void BrowsingContext::WalkPresContexts(Callback&& aCallback) { + PreOrderWalk([&](BrowsingContext* aContext) { + if (nsIDocShell* shell = aContext->GetDocShell()) { + if (RefPtr pc = shell->GetPresContext()) { + aCallback(pc.get()); + } + } + }); +} + +void BrowsingContext::PresContextAffectingFieldChanged() { + WalkPresContexts([&](nsPresContext* aPc) { + aPc->RecomputeBrowsingContextDependentData(); + }); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_SessionStoreEpoch>, + uint32_t aOldValue) { + if (!mCurrentWindowContext) { + return; + } + SessionStoreChild* sessionStoreChild = + SessionStoreChild::From(mCurrentWindowContext->GetWindowGlobalChild()); + if (!sessionStoreChild) { + return; + } + + sessionStoreChild->SetEpoch(GetSessionStoreEpoch()); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_GVAudibleAutoplayRequestStatus>) { + MOZ_ASSERT(IsTop(), + "Should only set GVAudibleAutoplayRequestStatus in the top-level " + "browsing context"); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_GVInaudibleAutoplayRequestStatus>) { + MOZ_ASSERT(IsTop(), + "Should only set GVAudibleAutoplayRequestStatus in the top-level " + "browsing context"); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_ExplicitActive>, + ExplicitActiveStatus aOldValue) { + const bool isActive = IsActive(); + const bool wasActive = [&] { + if (aOldValue != ExplicitActiveStatus::None) { + return aOldValue == ExplicitActiveStatus::Active; + } + return GetParent() && GetParent()->IsActive(); + }(); + + if (isActive == wasActive) { + return; + } + + if (IsTop()) { + Group()->UpdateToplevelsSuspendedIfNeeded(); + + if (XRE_IsParentProcess()) { + auto* bc = Canonical(); + if (BrowserParent* bp = bc->GetBrowserParent()) { + bp->RecomputeProcessPriority(); +#if defined(XP_WIN) && defined(ACCESSIBILITY) + if (a11y::Compatibility::IsDolphin()) { + // update active accessible documents on windows + if (a11y::DocAccessibleParent* tabDoc = + bp->GetTopLevelDocAccessible()) { + HWND window = tabDoc->GetEmulatedWindowHandle(); + MOZ_ASSERT(window); + if (window) { + if (isActive) { + a11y::nsWinUtils::ShowNativeWindow(window); + } else { + a11y::nsWinUtils::HideNativeWindow(window); + } + } + } + } +#endif + } + } + } + + PreOrderWalk([&](BrowsingContext* aContext) { + if (nsCOMPtr<nsIDocShell> ds = aContext->GetDocShell()) { + nsDocShell::Cast(ds)->ActivenessMaybeChanged(); + } + }); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_InRDMPane>, bool aOldValue) { + MOZ_ASSERT(IsTop(), + "Should only set InRDMPane in the top-level browsing context"); + if (GetInRDMPane() == aOldValue) { + return; + } + PresContextAffectingFieldChanged(); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_PageAwakeRequestCount>, + uint32_t aNewValue, ContentParent* aSource) { + return IsTop() && XRE_IsParentProcess() && !aSource; +} + +void BrowsingContext::DidSet(FieldIndex<IDX_PageAwakeRequestCount>, + uint32_t aOldValue) { + if (!IsTop() || aOldValue == GetPageAwakeRequestCount()) { + return; + } + Group()->UpdateToplevelsSuspendedIfNeeded(); +} + +auto BrowsingContext::CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue, + ContentParent* aSource) -> CanSetResult { + if (mozilla::SessionHistoryInParent()) { + return XRE_IsParentProcess() && !aSource ? CanSetResult::Allow + : CanSetResult::Deny; + } + + // Without Session History in Parent, session restore code still needs to set + // this from content processes. + return LegacyRevertIfNotOwningOrParentProcess(aSource); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue) { + RecomputeCanExecuteScripts(); +} + +void BrowsingContext::RecomputeCanExecuteScripts() { + const bool old = mCanExecuteScripts; + if (!AllowJavascript()) { + // Scripting has been explicitly disabled on our BrowsingContext. + mCanExecuteScripts = false; + } else if (GetParentWindowContext()) { + // Otherwise, inherit parent. + mCanExecuteScripts = GetParentWindowContext()->CanExecuteScripts(); + } else { + // Otherwise, we're the root of the tree, and we haven't explicitly disabled + // script. Allow. + mCanExecuteScripts = true; + } + + if (old != mCanExecuteScripts) { + for (WindowContext* wc : GetWindowContexts()) { + wc->RecomputeCanExecuteScripts(); + } + } +} + +bool BrowsingContext::InactiveForSuspend() const { + if (!StaticPrefs::dom_suspend_inactive_enabled()) { + return false; + } + // We should suspend a page only when it's inactive and doesn't have any awake + // request that is used to prevent page from being suspended because web page + // might still need to run their script. Eg. waiting for media keys to resume + // media, playing web audio, waiting in a video call conference room. + return !IsActive() && GetPageAwakeRequestCount() == 0; +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_TouchEventsOverrideInternal>, + dom::TouchEventsOverride, ContentParent* aSource) { + return XRE_IsParentProcess() && !aSource; +} + +void BrowsingContext::DidSet(FieldIndex<IDX_TouchEventsOverrideInternal>, + dom::TouchEventsOverride&& aOldValue) { + if (GetTouchEventsOverrideInternal() == aOldValue) { + return; + } + WalkPresContexts([&](nsPresContext* aPc) { + aPc->MediaFeatureValuesChanged( + {MediaFeatureChangeReason::SystemMetricsChange}, + // We're already iterating through sub documents, so we don't need to + // propagate the change again. + MediaFeatureChangePropagation::JustThisDocument); + }); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_EmbedderColorSchemes>, + EmbedderColorSchemes&& aOldValue) { + if (GetEmbedderColorSchemes() == aOldValue) { + return; + } + PresContextAffectingFieldChanged(); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_PrefersColorSchemeOverride>, + dom::PrefersColorSchemeOverride aOldValue) { + MOZ_ASSERT(IsTop()); + if (PrefersColorSchemeOverride() == aOldValue) { + return; + } + PresContextAffectingFieldChanged(); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_MediumOverride>, + nsString&& aOldValue) { + MOZ_ASSERT(IsTop()); + if (GetMediumOverride() == aOldValue) { + return; + } + PresContextAffectingFieldChanged(); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_DisplayMode>, + enum DisplayMode aOldValue) { + MOZ_ASSERT(IsTop()); + + if (GetDisplayMode() == aOldValue) { + return; + } + + WalkPresContexts([&](nsPresContext* aPc) { + aPc->MediaFeatureValuesChanged( + {MediaFeatureChangeReason::DisplayModeChange}, + // We're already iterating through sub documents, so we don't need + // to propagate the change again. + // + // Images and other resources don't change their display-mode + // evaluation, display-mode is a property of the browsing context. + MediaFeatureChangePropagation::JustThisDocument); + }); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_Muted>) { + MOZ_ASSERT(IsTop(), "Set muted flag on non top-level context!"); + USER_ACTIVATION_LOG("Set audio muted %d for %s browsing context 0x%08" PRIx64, + GetMuted(), XRE_IsParentProcess() ? "Parent" : "Child", + Id()); + PreOrderWalk([&](BrowsingContext* aContext) { + nsPIDOMWindowOuter* win = aContext->GetDOMWindow(); + if (win) { + win->RefreshMediaElementsVolume(); + } + }); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_IsAppTab>, const bool& aValue, + ContentParent* aSource) { + return XRE_IsParentProcess() && !aSource && IsTop(); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_HasSiblings>, const bool& aValue, + ContentParent* aSource) { + return XRE_IsParentProcess() && !aSource && IsTop(); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_ShouldDelayMediaFromStart>, + const bool& aValue, ContentParent* aSource) { + return IsTop(); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_ShouldDelayMediaFromStart>, + bool aOldValue) { + MOZ_ASSERT(IsTop(), "Set attribute on non top-level context!"); + if (aOldValue == GetShouldDelayMediaFromStart()) { + return; + } + if (!GetShouldDelayMediaFromStart()) { + PreOrderWalk([&](BrowsingContext* aContext) { + if (nsPIDOMWindowOuter* win = aContext->GetDOMWindow()) { + win->ActivateMediaComponents(); + } + }); + } +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_OverrideDPPX>, const float& aValue, + ContentParent* aSource) { + return XRE_IsParentProcess() && !aSource && IsTop(); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_OverrideDPPX>, float aOldValue) { + MOZ_ASSERT(IsTop()); + if (GetOverrideDPPX() == aOldValue) { + return; + } + PresContextAffectingFieldChanged(); +} + +void BrowsingContext::SetCustomUserAgent(const nsAString& aUserAgent, + ErrorResult& aRv) { + Top()->SetUserAgentOverride(aUserAgent, aRv); +} + +nsresult BrowsingContext::SetCustomUserAgent(const nsAString& aUserAgent) { + return Top()->SetUserAgentOverride(aUserAgent); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_UserAgentOverride>) { + MOZ_ASSERT(IsTop()); + + PreOrderWalk([&](BrowsingContext* aContext) { + nsIDocShell* shell = aContext->GetDocShell(); + if (shell) { + shell->ClearCachedUserAgent(); + } + }); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_IsInBFCache>, bool, + ContentParent* aSource) { + return IsTop() && !aSource && mozilla::BFCacheInParent(); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_IsInBFCache>) { + MOZ_RELEASE_ASSERT(mozilla::BFCacheInParent()); + MOZ_DIAGNOSTIC_ASSERT(IsTop()); + + const bool isInBFCache = GetIsInBFCache(); + if (!isInBFCache) { + UpdateCurrentTopByBrowserId(this); + PreOrderWalk([&](BrowsingContext* aContext) { + aContext->mIsInBFCache = false; + nsCOMPtr<nsIDocShell> shell = aContext->GetDocShell(); + if (shell) { + nsDocShell::Cast(shell)->ThawFreezeNonRecursive(true); + } + }); + } + + if (isInBFCache && XRE_IsContentProcess() && mDocShell) { + nsDocShell::Cast(mDocShell)->MaybeDisconnectChildListenersOnPageHide(); + } + + if (isInBFCache) { + PreOrderWalk([&](BrowsingContext* aContext) { + nsCOMPtr<nsIDocShell> shell = aContext->GetDocShell(); + if (shell) { + nsDocShell::Cast(shell)->FirePageHideShowNonRecursive(false); + } + }); + } else { + PostOrderWalk([&](BrowsingContext* aContext) { + nsCOMPtr<nsIDocShell> shell = aContext->GetDocShell(); + if (shell) { + nsDocShell::Cast(shell)->FirePageHideShowNonRecursive(true); + } + }); + } + + if (isInBFCache) { + PreOrderWalk([&](BrowsingContext* aContext) { + nsCOMPtr<nsIDocShell> shell = aContext->GetDocShell(); + if (shell) { + nsDocShell::Cast(shell)->ThawFreezeNonRecursive(false); + if (nsPresContext* pc = shell->GetPresContext()) { + pc->EventStateManager()->ResetHoverState(); + } + } + aContext->mIsInBFCache = true; + Document* doc = aContext->GetDocument(); + if (doc) { + // Notifying needs to happen after mIsInBFCache is set to true. + doc->NotifyActivityChanged(); + } + }); + + if (XRE_IsParentProcess()) { + if (mCurrentWindowContext && + mCurrentWindowContext->Canonical()->Fullscreen()) { + mCurrentWindowContext->Canonical()->ExitTopChromeDocumentFullscreen(); + } + } + } +} + +void BrowsingContext::DidSet(FieldIndex<IDX_SyntheticDocumentContainer>) { + if (WindowContext* parentWindowContext = GetParentWindowContext()) { + parentWindowContext->UpdateChildSynthetic(this, + GetSyntheticDocumentContainer()); + } +} + +void BrowsingContext::SetCustomPlatform(const nsAString& aPlatform, + ErrorResult& aRv) { + Top()->SetPlatformOverride(aPlatform, aRv); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_PlatformOverride>) { + MOZ_ASSERT(IsTop()); + + PreOrderWalk([&](BrowsingContext* aContext) { + nsIDocShell* shell = aContext->GetDocShell(); + if (shell) { + shell->ClearCachedPlatform(); + } + }); +} + +auto BrowsingContext::LegacyRevertIfNotOwningOrParentProcess( + ContentParent* aSource) -> CanSetResult { + if (aSource) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!Canonical()->IsOwnedByProcess(aSource->ChildID())) { + return CanSetResult::Revert; + } + } else if (!IsInProcess() && !XRE_IsParentProcess()) { + // Don't allow this to be set from content processes that + // don't own the BrowsingContext. + return CanSetResult::Deny; + } + + return CanSetResult::Allow; +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_IsActiveBrowserWindowInternal>, + const bool& aValue, ContentParent* aSource) { + // Should only be set in the parent process. + return XRE_IsParentProcess() && !aSource && IsTop(); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_IsActiveBrowserWindowInternal>, + bool aOldValue) { + bool isActivateEvent = GetIsActiveBrowserWindowInternal(); + // The browser window containing this context has changed + // activation state so update window inactive document states + // for all in-process documents. + PreOrderWalk([isActivateEvent](BrowsingContext* aContext) { + if (RefPtr<Document> doc = aContext->GetExtantDocument()) { + doc->UpdateDocumentStates(DocumentState::WINDOW_INACTIVE, true); + + RefPtr<nsPIDOMWindowInner> win = doc->GetInnerWindow(); + if (win) { + RefPtr<MediaDevices> devices; + if (isActivateEvent && (devices = win->GetExtantMediaDevices())) { + devices->BrowserWindowBecameActive(); + } + + if (XRE_IsContentProcess() && + (!aContext->GetParent() || !aContext->GetParent()->IsInProcess())) { + // Send the inner window an activate/deactivate event if + // the context is the top of a sub-tree of in-process + // contexts. + nsContentUtils::DispatchEventOnlyToChrome( + doc, win, isActivateEvent ? u"activate"_ns : u"deactivate"_ns, + CanBubble::eYes, Cancelable::eYes, nullptr); + } + } + } + }); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_OpenerPolicy>, + nsILoadInfo::CrossOriginOpenerPolicy aPolicy, + ContentParent* aSource) { + // A potentially cross-origin isolated BC can't change opener policy, nor can + // a BC become potentially cross-origin isolated. An unchanged policy is + // always OK. + return GetOpenerPolicy() == aPolicy || + (GetOpenerPolicy() != + nsILoadInfo:: + OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP && + aPolicy != + nsILoadInfo:: + OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP); +} + +auto BrowsingContext::CanSet(FieldIndex<IDX_AllowContentRetargeting>, + const bool& aAllowContentRetargeting, + ContentParent* aSource) -> CanSetResult { + return LegacyRevertIfNotOwningOrParentProcess(aSource); +} + +auto BrowsingContext::CanSet(FieldIndex<IDX_AllowContentRetargetingOnChildren>, + const bool& aAllowContentRetargetingOnChildren, + ContentParent* aSource) -> CanSetResult { + return LegacyRevertIfNotOwningOrParentProcess(aSource); +} + +auto BrowsingContext::CanSet(FieldIndex<IDX_AllowPlugins>, + const bool& aAllowPlugins, ContentParent* aSource) + -> CanSetResult { + return LegacyRevertIfNotOwningOrParentProcess(aSource); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_FullscreenAllowedByOwner>, + const bool& aAllowed, ContentParent* aSource) { + return CheckOnlyEmbedderCanSet(aSource); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_UseErrorPages>, + const bool& aUseErrorPages, + ContentParent* aSource) { + return CheckOnlyEmbedderCanSet(aSource); +} + +TouchEventsOverride BrowsingContext::TouchEventsOverride() const { + for (const auto* bc = this; bc; bc = bc->GetParent()) { + auto tev = bc->GetTouchEventsOverrideInternal(); + if (tev != dom::TouchEventsOverride::None) { + return tev; + } + } + return dom::TouchEventsOverride::None; +} + +bool BrowsingContext::TargetTopLevelLinkClicksToBlank() const { + return Top()->GetTargetTopLevelLinkClicksToBlankInternal(); +} + +// We map `watchedByDevTools` WebIDL attribute to `watchedByDevToolsInternal` +// BC field. And we map it to the top level BrowsingContext. +bool BrowsingContext::WatchedByDevTools() { + return Top()->GetWatchedByDevToolsInternal(); +} + +// Enforce that the watchedByDevTools BC field can only be set on the top level +// Browsing Context. +bool BrowsingContext::CanSet(FieldIndex<IDX_WatchedByDevToolsInternal>, + const bool& aWatchedByDevTools, + ContentParent* aSource) { + return IsTop(); +} +void BrowsingContext::SetWatchedByDevTools(bool aWatchedByDevTools, + ErrorResult& aRv) { + if (!IsTop()) { + aRv.ThrowInvalidModificationError( + "watchedByDevTools can only be set on top BrowsingContext"); + return; + } + SetWatchedByDevToolsInternal(aWatchedByDevTools, aRv); +} + +auto BrowsingContext::CanSet(FieldIndex<IDX_DefaultLoadFlags>, + const uint32_t& aDefaultLoadFlags, + ContentParent* aSource) -> CanSetResult { + // Bug 1623565 - Are these flags only used by the debugger, which makes it + // possible that this field can only be settable by the parent process? + return LegacyRevertIfNotOwningOrParentProcess(aSource); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_DefaultLoadFlags>) { + auto loadFlags = GetDefaultLoadFlags(); + if (GetDocShell()) { + nsDocShell::Cast(GetDocShell())->SetLoadGroupDefaultLoadFlags(loadFlags); + } + + if (XRE_IsParentProcess()) { + PreOrderWalk([&](BrowsingContext* aContext) { + if (aContext != this) { + // Setting load flags on a discarded context has no effect. + Unused << aContext->SetDefaultLoadFlags(loadFlags); + } + }); + } +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_UseGlobalHistory>, + const bool& aUseGlobalHistory, + ContentParent* aSource) { + // Should only be set in the parent process. + // return XRE_IsParentProcess() && !aSource; + return true; +} + +auto BrowsingContext::CanSet(FieldIndex<IDX_UserAgentOverride>, + const nsString& aUserAgent, ContentParent* aSource) + -> CanSetResult { + if (!IsTop()) { + return CanSetResult::Deny; + } + + return LegacyRevertIfNotOwningOrParentProcess(aSource); +} + +auto BrowsingContext::CanSet(FieldIndex<IDX_PlatformOverride>, + const nsString& aPlatform, ContentParent* aSource) + -> CanSetResult { + if (!IsTop()) { + return CanSetResult::Deny; + } + + return LegacyRevertIfNotOwningOrParentProcess(aSource); +} + +bool BrowsingContext::CheckOnlyEmbedderCanSet(ContentParent* aSource) { + if (XRE_IsParentProcess()) { + uint64_t childId = aSource ? aSource->ChildID() : 0; + return Canonical()->IsEmbeddedInProcess(childId); + } + return mEmbeddedByThisProcess; +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_EmbedderInnerWindowId>, + const uint64_t& aValue, ContentParent* aSource) { + // If we have a parent window, our embedder inner window ID must match it. + if (mParentWindow) { + return mParentWindow->Id() == aValue; + } + + // For toplevel BrowsingContext instances, this value may only be set by the + // parent process, or initialized to `0`. + return CheckOnlyEmbedderCanSet(aSource); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_EmbedderElementType>, + const Maybe<nsString>&, ContentParent* aSource) { + return CheckOnlyEmbedderCanSet(aSource); +} + +auto BrowsingContext::CanSet(FieldIndex<IDX_CurrentInnerWindowId>, + const uint64_t& aValue, ContentParent* aSource) + -> CanSetResult { + // Generally allow clearing this. We may want to be more precise about this + // check in the future. + if (aValue == 0) { + return CanSetResult::Allow; + } + + // We must have access to the specified context. + RefPtr<WindowContext> window = WindowContext::GetById(aValue); + if (!window || window->GetBrowsingContext() != this) { + return CanSetResult::Deny; + } + + if (aSource) { + // If the sending process is no longer the current owner, revert + MOZ_ASSERT(XRE_IsParentProcess()); + if (!Canonical()->IsOwnedByProcess(aSource->ChildID())) { + return CanSetResult::Revert; + } + } else if (XRE_IsContentProcess() && !IsOwnedByProcess()) { + return CanSetResult::Deny; + } + + return CanSetResult::Allow; +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_ParentInitiatedNavigationEpoch>, + const uint64_t& aValue, ContentParent* aSource) { + return XRE_IsParentProcess() && !aSource; +} + +void BrowsingContext::DidSet(FieldIndex<IDX_CurrentInnerWindowId>) { + RefPtr<WindowContext> prevWindowContext = mCurrentWindowContext.forget(); + mCurrentWindowContext = WindowContext::GetById(GetCurrentInnerWindowId()); + MOZ_ASSERT( + !mCurrentWindowContext || mWindowContexts.Contains(mCurrentWindowContext), + "WindowContext not registered?"); + + // Clear our cached `children` value, to ensure that JS sees the up-to-date + // value. + BrowsingContext_Binding::ClearCachedChildrenValue(this); + + if (XRE_IsParentProcess()) { + if (prevWindowContext != mCurrentWindowContext) { + if (prevWindowContext) { + prevWindowContext->Canonical()->DidBecomeCurrentWindowGlobal(false); + } + if (mCurrentWindowContext) { + // We set a timer when we set the current inner window. This + // will then flush the session storage to session store to + // make sure that we don't miss to store session storage to + // session store that is a result of navigation. This is due + // to Bug 1700623. We wish to fix this in Bug 1711886, where + // making sure to store everything would make this timer + // unnecessary. + Canonical()->MaybeScheduleSessionStoreUpdate(); + mCurrentWindowContext->Canonical()->DidBecomeCurrentWindowGlobal(true); + } + } + BrowserParent::UpdateFocusFromBrowsingContext(); + } +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_IsPopupSpam>, const bool& aValue, + ContentParent* aSource) { + // Ensure that we only mark a browsing context as popup spam once and never + // unmark it. + return aValue && !GetIsPopupSpam(); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_IsPopupSpam>) { + if (GetIsPopupSpam()) { + PopupBlocker::RegisterOpenPopupSpam(); + } +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_MessageManagerGroup>, + const nsString& aMessageManagerGroup, + ContentParent* aSource) { + // Should only be set in the parent process on toplevel. + return XRE_IsParentProcess() && !aSource && IsTopContent(); +} + +bool BrowsingContext::CanSet( + FieldIndex<IDX_OrientationLock>, + const mozilla::hal::ScreenOrientation& aOrientationLock, + ContentParent* aSource) { + return IsTop(); +} + +bool BrowsingContext::IsLoading() { + if (GetLoading()) { + return true; + } + + // If we're in the same process as the page, we're possibly just + // updating the flag. + nsIDocShell* shell = GetDocShell(); + if (shell) { + Document* doc = shell->GetDocument(); + return doc && doc->GetReadyStateEnum() < Document::READYSTATE_COMPLETE; + } + + return false; +} + +void BrowsingContext::DidSet(FieldIndex<IDX_Loading>) { + if (mFields.Get<IDX_Loading>()) { + return; + } + + while (!mDeprioritizedLoadRunner.isEmpty()) { + nsCOMPtr<nsIRunnable> runner = mDeprioritizedLoadRunner.popFirst(); + NS_DispatchToCurrentThread(runner.forget()); + } + + if (StaticPrefs::dom_separate_event_queue_for_post_message_enabled() && + Top() == this) { + Group()->FlushPostMessageEvents(); + } +} + +// Inform the Document for this context of the (potential) change in +// loading state +void BrowsingContext::DidSet(FieldIndex<IDX_AncestorLoading>) { + nsPIDOMWindowOuter* outer = GetDOMWindow(); + if (!outer) { + MOZ_LOG(gTimeoutDeferralLog, mozilla::LogLevel::Debug, + ("DidSetAncestorLoading BC: %p -- No outer window", (void*)this)); + return; + } + Document* document = nsGlobalWindowOuter::Cast(outer)->GetExtantDoc(); + if (document) { + MOZ_LOG(gTimeoutDeferralLog, mozilla::LogLevel::Debug, + ("DidSetAncestorLoading BC: %p -- NotifyLoading(%d, %d, %d)", + (void*)this, GetAncestorLoading(), document->GetReadyStateEnum(), + document->GetReadyStateEnum())); + document->NotifyLoading(GetAncestorLoading(), document->GetReadyStateEnum(), + document->GetReadyStateEnum()); + } +} + +void BrowsingContext::DidSet(FieldIndex<IDX_AuthorStyleDisabledDefault>) { + MOZ_ASSERT(IsTop(), + "Should only set AuthorStyleDisabledDefault in the top " + "browsing context"); + + // We don't need to handle changes to this field, since PageStyleChild.sys.mjs + // will respond to the PageStyle:Disable message in all content processes. + // + // But we store the state here on the top BrowsingContext so that the + // docshell has somewhere to look for the current author style disabling + // state when new iframes are inserted. +} + +void BrowsingContext::DidSet(FieldIndex<IDX_TextZoom>, float aOldValue) { + if (GetTextZoom() == aOldValue) { + return; + } + + if (IsInProcess()) { + if (nsIDocShell* shell = GetDocShell()) { + if (nsPresContext* pc = shell->GetPresContext()) { + pc->RecomputeBrowsingContextDependentData(); + } + } + + for (BrowsingContext* child : Children()) { + // Setting text zoom on a discarded context has no effect. + Unused << child->SetTextZoom(GetTextZoom()); + } + } + + if (IsTop() && XRE_IsParentProcess()) { + if (Element* element = GetEmbedderElement()) { + AsyncEventDispatcher::RunDOMEventWhenSafe(*element, u"TextZoomChange"_ns, + CanBubble::eYes, + ChromeOnlyDispatch::eYes); + } + } +} + +// TODO(emilio): It'd be potentially nicer and cheaper to allow to set this only +// on the Top() browsing context, but there are a lot of tests that rely on +// zooming a subframe so... +void BrowsingContext::DidSet(FieldIndex<IDX_FullZoom>, float aOldValue) { + if (GetFullZoom() == aOldValue) { + return; + } + + if (IsInProcess()) { + if (nsIDocShell* shell = GetDocShell()) { + if (nsPresContext* pc = shell->GetPresContext()) { + pc->RecomputeBrowsingContextDependentData(); + } + } + + for (BrowsingContext* child : Children()) { + // Setting full zoom on a discarded context has no effect. + Unused << child->SetFullZoom(GetFullZoom()); + } + } + + if (IsTop() && XRE_IsParentProcess()) { + if (Element* element = GetEmbedderElement()) { + AsyncEventDispatcher::RunDOMEventWhenSafe(*element, u"FullZoomChange"_ns, + CanBubble::eYes, + ChromeOnlyDispatch::eYes); + } + } +} + +void BrowsingContext::AddDeprioritizedLoadRunner(nsIRunnable* aRunner) { + MOZ_ASSERT(IsLoading()); + MOZ_ASSERT(Top() == this); + + RefPtr<DeprioritizedLoadRunner> runner = new DeprioritizedLoadRunner(aRunner); + mDeprioritizedLoadRunner.insertBack(runner); + NS_DispatchToCurrentThreadQueue( + runner.forget(), StaticPrefs::page_load_deprioritization_period(), + EventQueuePriority::Idle); +} + +bool BrowsingContext::IsDynamic() const { + const BrowsingContext* current = this; + do { + if (current->CreatedDynamically()) { + return true; + } + } while ((current = current->GetParent())); + + return false; +} + +bool BrowsingContext::GetOffsetPath(nsTArray<uint32_t>& aPath) const { + for (const BrowsingContext* current = this; current && current->GetParent(); + current = current->GetParent()) { + if (current->CreatedDynamically()) { + return false; + } + aPath.AppendElement(current->ChildOffset()); + } + return true; +} + +void BrowsingContext::GetHistoryID(JSContext* aCx, + JS::MutableHandle<JS::Value> aVal, + ErrorResult& aError) { + if (!xpc::ID2JSValue(aCx, GetHistoryID(), aVal)) { + aError.Throw(NS_ERROR_OUT_OF_MEMORY); + } +} + +void BrowsingContext::InitSessionHistory() { + MOZ_ASSERT(!IsDiscarded()); + MOZ_ASSERT(IsTop()); + MOZ_ASSERT(EverAttached()); + + if (!GetHasSessionHistory()) { + MOZ_ALWAYS_SUCCEEDS(SetHasSessionHistory(true)); + } +} + +ChildSHistory* BrowsingContext::GetChildSessionHistory() { + if (!mozilla::SessionHistoryInParent()) { + // For now we're checking that the session history object for the child + // process is available before returning the ChildSHistory object, because + // it is the actual implementation that ChildSHistory forwards to. This can + // be removed once session history is stored exclusively in the parent + // process. + return mChildSessionHistory && mChildSessionHistory->IsInProcess() + ? mChildSessionHistory.get() + : nullptr; + } + + return mChildSessionHistory; +} + +void BrowsingContext::CreateChildSHistory() { + MOZ_ASSERT(IsTop()); + MOZ_ASSERT(GetHasSessionHistory()); + MOZ_DIAGNOSTIC_ASSERT(!mChildSessionHistory); + + // Because session history is global in a browsing context tree, every process + // that has access to a browsing context tree needs access to its session + // history. That is why we create the ChildSHistory object in every process + // where we have access to this browsing context (which is the top one). + mChildSessionHistory = new ChildSHistory(this); + + // If the top browsing context (this one) is loaded in this process then we + // also create the session history implementation for the child process. + // This can be removed once session history is stored exclusively in the + // parent process. + mChildSessionHistory->SetIsInProcess(IsInProcess()); +} + +void BrowsingContext::DidSet(FieldIndex<IDX_HasSessionHistory>, + bool aOldValue) { + MOZ_ASSERT(GetHasSessionHistory() || !aOldValue, + "We don't support turning off session history."); + + if (GetHasSessionHistory() && !aOldValue) { + CreateChildSHistory(); + } +} + +bool BrowsingContext::CanSet( + FieldIndex<IDX_TargetTopLevelLinkClicksToBlankInternal>, + const bool& aTargetTopLevelLinkClicksToBlankInternal, + ContentParent* aSource) { + return XRE_IsParentProcess() && !aSource && IsTop(); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue, + ContentParent* aSource) { + // We should only be able to set this for toplevel contexts which don't have + // an ID yet. + return GetBrowserId() == 0 && IsTop() && Children().IsEmpty(); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_PendingInitialization>, + bool aNewValue, ContentParent* aSource) { + // Can only be cleared from `true` to `false`, and should only ever be set on + // the toplevel BrowsingContext. + return IsTop() && GetPendingInitialization() && !aNewValue; +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_HasRestoreData>, bool aNewValue, + ContentParent* aSource) { + return IsTop(); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>, + const bool& aIsUnderHiddenEmbedderElement, + ContentParent* aSource) { + return true; +} + +void BrowsingContext::DidSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>, + bool aOldValue) { + nsIDocShell* shell = GetDocShell(); + if (!shell) { + return; + } + + const bool newValue = IsUnderHiddenEmbedderElement(); + if (NS_WARN_IF(aOldValue == newValue)) { + return; + } + if (PresShell* presShell = shell->GetPresShell()) { + presShell->SetIsUnderHiddenEmbedderElement(newValue); + } + + // Propagate to children. + for (BrowsingContext* child : Children()) { + Element* embedderElement = child->GetEmbedderElement(); + if (!embedderElement) { + // TODO: We shouldn't need to null check here since `child` and the + // element returned by `child->GetEmbedderElement()` are in our + // process (the actual browsing context represented by `child` may not + // be, but that doesn't matter). However, there are currently a very + // small number of crashes due to `embedderElement` being null, somehow + // - see bug 1551241. For now we wallpaper the crash. + continue; + } + + bool embedderFrameIsHidden = true; + if (auto embedderFrame = embedderElement->GetPrimaryFrame()) { + embedderFrameIsHidden = !embedderFrame->StyleVisibility()->IsVisible(); + } + + bool hidden = IsUnderHiddenEmbedderElement() || embedderFrameIsHidden; + if (child->IsUnderHiddenEmbedderElement() != hidden) { + Unused << child->SetIsUnderHiddenEmbedderElement(hidden); + } + } +} + +bool BrowsingContext::IsPopupAllowed() { + for (auto* context = GetCurrentWindowContext(); context; + context = context->GetParentWindowContext()) { + if (context->CanShowPopup()) { + return true; + } + } + + return false; +} + +/* static */ +bool BrowsingContext::ShouldAddEntryForRefresh( + nsIURI* aPreviousURI, const SessionHistoryInfo& aInfo) { + return ShouldAddEntryForRefresh(aPreviousURI, aInfo.GetURI(), + aInfo.HasPostData()); +} + +/* static */ +bool BrowsingContext::ShouldAddEntryForRefresh(nsIURI* aPreviousURI, + nsIURI* aNewURI, + bool aHasPostData) { + if (aHasPostData) { + return true; + } + + bool equalsURI = false; + if (aPreviousURI) { + aPreviousURI->Equals(aNewURI, &equalsURI); + } + return !equalsURI; +} + +void BrowsingContext::SessionHistoryCommit( + const LoadingSessionHistoryInfo& aInfo, uint32_t aLoadType, + nsIURI* aPreviousURI, SessionHistoryInfo* aPreviousActiveEntry, + bool aPersist, bool aCloneEntryChildren, bool aChannelExpired, + uint32_t aCacheKey) { + nsID changeID = {}; + if (XRE_IsContentProcess()) { + RefPtr<ChildSHistory> rootSH = Top()->GetChildSessionHistory(); + if (rootSH) { + if (!aInfo.mLoadIsFromSessionHistory) { + // We try to mimic as closely as possible what will happen in + // CanonicalBrowsingContext::SessionHistoryCommit. We'll be + // incrementing the session history length if we're not replacing, + // this is a top-level load or it's not the initial load in an iframe, + // ShouldUpdateSessionHistory(loadType) returns true and it's not a + // refresh for which ShouldAddEntryForRefresh returns false. + // It is possible that this leads to wrong length temporarily, but + // so would not having the check for replace. + // Note that nsSHistory::AddEntry does a replace load if the current + // entry is not marked as a persisted entry. The child process does + // not have access to the current entry, so we use the previous active + // entry as the best approximation. When that's not the current entry + // then the length might be wrong briefly, until the parent process + // commits the actual length. + if (!LOAD_TYPE_HAS_FLAGS( + aLoadType, nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY) && + (IsTop() + ? (!aPreviousActiveEntry || aPreviousActiveEntry->GetPersist()) + : !!aPreviousActiveEntry) && + ShouldUpdateSessionHistory(aLoadType) && + (!LOAD_TYPE_HAS_FLAGS(aLoadType, + nsIWebNavigation::LOAD_FLAGS_IS_REFRESH) || + ShouldAddEntryForRefresh(aPreviousURI, aInfo.mInfo))) { + changeID = rootSH->AddPendingHistoryChange(); + } + } else { + // History load doesn't change the length, only index. + changeID = rootSH->AddPendingHistoryChange(aInfo.mOffset, 0); + } + } + ContentChild* cc = ContentChild::GetSingleton(); + mozilla::Unused << cc->SendHistoryCommit( + this, aInfo.mLoadId, changeID, aLoadType, aPersist, aCloneEntryChildren, + aChannelExpired, aCacheKey); + } else { + Canonical()->SessionHistoryCommit(aInfo.mLoadId, changeID, aLoadType, + aPersist, aCloneEntryChildren, + aChannelExpired, aCacheKey); + } +} + +void BrowsingContext::SetActiveSessionHistoryEntry( + const Maybe<nsPoint>& aPreviousScrollPos, SessionHistoryInfo* aInfo, + uint32_t aLoadType, uint32_t aUpdatedCacheKey, bool aUpdateLength) { + if (XRE_IsContentProcess()) { + // XXX Why we update cache key only in content process case? + if (aUpdatedCacheKey != 0) { + aInfo->SetCacheKey(aUpdatedCacheKey); + } + + nsID changeID = {}; + if (aUpdateLength) { + RefPtr<ChildSHistory> shistory = Top()->GetChildSessionHistory(); + if (shistory) { + changeID = shistory->AddPendingHistoryChange(); + } + } + ContentChild::GetSingleton()->SendSetActiveSessionHistoryEntry( + this, aPreviousScrollPos, *aInfo, aLoadType, aUpdatedCacheKey, + changeID); + } else { + Canonical()->SetActiveSessionHistoryEntry( + aPreviousScrollPos, aInfo, aLoadType, aUpdatedCacheKey, nsID()); + } +} + +void BrowsingContext::ReplaceActiveSessionHistoryEntry( + SessionHistoryInfo* aInfo) { + if (XRE_IsContentProcess()) { + ContentChild::GetSingleton()->SendReplaceActiveSessionHistoryEntry(this, + *aInfo); + } else { + Canonical()->ReplaceActiveSessionHistoryEntry(aInfo); + } +} + +void BrowsingContext::RemoveDynEntriesFromActiveSessionHistoryEntry() { + if (XRE_IsContentProcess()) { + ContentChild::GetSingleton() + ->SendRemoveDynEntriesFromActiveSessionHistoryEntry(this); + } else { + Canonical()->RemoveDynEntriesFromActiveSessionHistoryEntry(); + } +} + +void BrowsingContext::RemoveFromSessionHistory(const nsID& aChangeID) { + if (XRE_IsContentProcess()) { + ContentChild::GetSingleton()->SendRemoveFromSessionHistory(this, aChangeID); + } else { + Canonical()->RemoveFromSessionHistory(aChangeID); + } +} + +void BrowsingContext::HistoryGo( + int32_t aOffset, uint64_t aHistoryEpoch, bool aRequireUserInteraction, + bool aUserActivation, std::function<void(Maybe<int32_t>&&)>&& aResolver) { + if (XRE_IsContentProcess()) { + ContentChild::GetSingleton()->SendHistoryGo( + this, aOffset, aHistoryEpoch, aRequireUserInteraction, aUserActivation, + std::move(aResolver), + [](mozilla::ipc:: + ResponseRejectReason) { /* FIXME Is ignoring this fine? */ }); + } else { + RefPtr<CanonicalBrowsingContext> self = Canonical(); + aResolver(self->HistoryGo( + aOffset, aHistoryEpoch, aRequireUserInteraction, aUserActivation, + Canonical()->GetContentParent() + ? Some(Canonical()->GetContentParent()->ChildID()) + : Nothing())); + } +} + +void BrowsingContext::SetChildSHistory(ChildSHistory* aChildSHistory) { + mChildSessionHistory = aChildSHistory; + mChildSessionHistory->SetBrowsingContext(this); + mFields.SetWithoutSyncing<IDX_HasSessionHistory>(true); +} + +bool BrowsingContext::ShouldUpdateSessionHistory(uint32_t aLoadType) { + // We don't update session history on reload unless we're loading + // an iframe in shift-reload case. + return nsDocShell::ShouldUpdateGlobalHistory(aLoadType) && + (!(aLoadType & nsIDocShell::LOAD_CMD_RELOAD) || + (IsForceReloadType(aLoadType) && IsSubframe())); +} + +nsresult BrowsingContext::CheckLocationChangeRateLimit(CallerType aCallerType) { + // We only rate limit non system callers + if (aCallerType == CallerType::System) { + return NS_OK; + } + + // Fetch rate limiting preferences + uint32_t limitCount = + StaticPrefs::dom_navigation_locationChangeRateLimit_count(); + uint32_t timeSpanSeconds = + StaticPrefs::dom_navigation_locationChangeRateLimit_timespan(); + + // Disable throttling if either of the preferences is set to 0. + if (limitCount == 0 || timeSpanSeconds == 0) { + return NS_OK; + } + + TimeDuration throttleSpan = TimeDuration::FromSeconds(timeSpanSeconds); + + if (mLocationChangeRateLimitSpanStart.IsNull() || + ((TimeStamp::Now() - mLocationChangeRateLimitSpanStart) > throttleSpan)) { + // Initial call or timespan exceeded, reset counter and timespan. + mLocationChangeRateLimitSpanStart = TimeStamp::Now(); + mLocationChangeRateLimitCount = 1; + return NS_OK; + } + + if (mLocationChangeRateLimitCount >= limitCount) { + // Rate limit reached + + Document* doc = GetDocument(); + if (doc) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, "DOM"_ns, doc, + nsContentUtils::eDOM_PROPERTIES, + "LocChangeFloodingPrevented"); + } + + return NS_ERROR_DOM_SECURITY_ERR; + } + + mLocationChangeRateLimitCount++; + return NS_OK; +} + +void BrowsingContext::ResetLocationChangeRateLimit() { + // Resetting the timestamp object will cause the check function to + // init again and reset the rate limit. + mLocationChangeRateLimitSpanStart = TimeStamp(); +} + +} // namespace dom + +namespace ipc { + +void IPDLParamTraits<dom::MaybeDiscarded<dom::BrowsingContext>>::Write( + IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::MaybeDiscarded<dom::BrowsingContext>& aParam) { + MOZ_DIAGNOSTIC_ASSERT(!aParam.GetMaybeDiscarded() || + aParam.GetMaybeDiscarded()->EverAttached()); + uint64_t id = aParam.ContextId(); + WriteIPDLParam(aWriter, aActor, id); +} + +bool IPDLParamTraits<dom::MaybeDiscarded<dom::BrowsingContext>>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + dom::MaybeDiscarded<dom::BrowsingContext>* aResult) { + uint64_t id = 0; + if (!ReadIPDLParam(aReader, aActor, &id)) { + return false; + } + + if (id == 0) { + *aResult = nullptr; + } else if (RefPtr<dom::BrowsingContext> bc = dom::BrowsingContext::Get(id)) { + *aResult = std::move(bc); + } else { + aResult->SetDiscarded(id); + } + return true; +} + +void IPDLParamTraits<dom::BrowsingContext::IPCInitializer>::Write( + IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::BrowsingContext::IPCInitializer& aInit) { + // Write actor ID parameters. + WriteIPDLParam(aWriter, aActor, aInit.mId); + WriteIPDLParam(aWriter, aActor, aInit.mParentId); + WriteIPDLParam(aWriter, aActor, aInit.mWindowless); + WriteIPDLParam(aWriter, aActor, aInit.mUseRemoteTabs); + WriteIPDLParam(aWriter, aActor, aInit.mUseRemoteSubframes); + WriteIPDLParam(aWriter, aActor, aInit.mCreatedDynamically); + WriteIPDLParam(aWriter, aActor, aInit.mChildOffset); + WriteIPDLParam(aWriter, aActor, aInit.mOriginAttributes); + WriteIPDLParam(aWriter, aActor, aInit.mRequestContextId); + WriteIPDLParam(aWriter, aActor, aInit.mSessionHistoryIndex); + WriteIPDLParam(aWriter, aActor, aInit.mSessionHistoryCount); + WriteIPDLParam(aWriter, aActor, aInit.mFields); +} + +bool IPDLParamTraits<dom::BrowsingContext::IPCInitializer>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + dom::BrowsingContext::IPCInitializer* aInit) { + // Read actor ID parameters. + if (!ReadIPDLParam(aReader, aActor, &aInit->mId) || + !ReadIPDLParam(aReader, aActor, &aInit->mParentId) || + !ReadIPDLParam(aReader, aActor, &aInit->mWindowless) || + !ReadIPDLParam(aReader, aActor, &aInit->mUseRemoteTabs) || + !ReadIPDLParam(aReader, aActor, &aInit->mUseRemoteSubframes) || + !ReadIPDLParam(aReader, aActor, &aInit->mCreatedDynamically) || + !ReadIPDLParam(aReader, aActor, &aInit->mChildOffset) || + !ReadIPDLParam(aReader, aActor, &aInit->mOriginAttributes) || + !ReadIPDLParam(aReader, aActor, &aInit->mRequestContextId) || + !ReadIPDLParam(aReader, aActor, &aInit->mSessionHistoryIndex) || + !ReadIPDLParam(aReader, aActor, &aInit->mSessionHistoryCount) || + !ReadIPDLParam(aReader, aActor, &aInit->mFields)) { + return false; + } + return true; +} + +template struct IPDLParamTraits<dom::BrowsingContext::BaseTransaction>; + +} // namespace ipc +} // namespace mozilla diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h new file mode 100644 index 0000000000..4c245337b7 --- /dev/null +++ b/docshell/base/BrowsingContext.h @@ -0,0 +1,1458 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_BrowsingContext_h +#define mozilla_dom_BrowsingContext_h + +#include <tuple> +#include "GVAutoplayRequestUtils.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/HalScreenConfiguration.h" +#include "mozilla/LinkedList.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Span.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/LocationBase.h" +#include "mozilla/dom/MaybeDiscarded.h" +#include "mozilla/dom/PopupBlocker.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/BrowsingContextBinding.h" +#include "mozilla/dom/ScreenOrientationBinding.h" +#include "mozilla/dom/SyncedContext.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIDocShell.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" +#include "nsILoadInfo.h" +#include "nsILoadContext.h" +#include "nsThreadUtils.h" + +class nsDocShellLoadState; +class nsGlobalWindowInner; +class nsGlobalWindowOuter; +class nsIPrincipal; +class nsOuterWindowProxy; +struct nsPoint; +class PickleIterator; + +namespace IPC { +class Message; +class MessageReader; +class MessageWriter; +} // namespace IPC + +namespace mozilla { + +class ErrorResult; +class LogModule; + +namespace ipc { +class IProtocol; +class IPCResult; + +template <typename T> +struct IPDLParamTraits; +} // namespace ipc + +namespace dom { +class BrowsingContent; +class BrowsingContextGroup; +class CanonicalBrowsingContext; +class ChildSHistory; +class ContentParent; +class Element; +struct LoadingSessionHistoryInfo; +template <typename> +struct Nullable; +template <typename T> +class Sequence; +class SessionHistoryInfo; +class SessionStorageManager; +class StructuredCloneHolder; +class WindowContext; +class WindowGlobalChild; +struct WindowPostMessageOptions; +class WindowProxyHolder; + +enum class ExplicitActiveStatus : uint8_t { + None, + Active, + Inactive, + EndGuard_, +}; + +struct EmbedderColorSchemes { + PrefersColorSchemeOverride mUsed{}; + PrefersColorSchemeOverride mPreferred{}; + + bool operator==(const EmbedderColorSchemes& aOther) const { + return mUsed == aOther.mUsed && mPreferred == aOther.mPreferred; + } + + bool operator!=(const EmbedderColorSchemes& aOther) const { + return !(*this == aOther); + } +}; + +// Fields are, by default, settable by any process and readable by any process. +// Racy sets will be resolved as-if they occurred in the order the parent +// process finds out about them. +// +// The `DidSet` and `CanSet` methods may be overloaded to provide different +// behavior for a specific field. +// * `DidSet` is called to run code in every process whenever the value is +// updated (This currently occurs even if the value didn't change, though +// this may change in the future). +// * `CanSet` is called before attempting to set the value, in both the process +// which calls `Set`, and the parent process, and will kill the misbehaving +// process if it fails. +#define MOZ_EACH_BC_FIELD(FIELD) \ + FIELD(Name, nsString) \ + FIELD(Closed, bool) \ + FIELD(ExplicitActive, ExplicitActiveStatus) \ + /* Top()-only. If true, new-playing media will be suspended when in an \ + * inactive browsing context. */ \ + FIELD(SuspendMediaWhenInactive, bool) \ + /* If true, we're within the nested event loop in window.open, and this \ + * context may not be used as the target of a load */ \ + FIELD(PendingInitialization, bool) \ + /* Indicates if the browser window is active for the purpose of the \ + * :-moz-window-inactive pseudoclass. Only read from or set on the \ + * top BrowsingContext. */ \ + FIELD(IsActiveBrowserWindowInternal, bool) \ + FIELD(OpenerPolicy, nsILoadInfo::CrossOriginOpenerPolicy) \ + /* Current opener for the BrowsingContext. Weak reference */ \ + FIELD(OpenerId, uint64_t) \ + FIELD(OnePermittedSandboxedNavigatorId, uint64_t) \ + /* WindowID of the inner window which embeds this BC */ \ + FIELD(EmbedderInnerWindowId, uint64_t) \ + FIELD(CurrentInnerWindowId, uint64_t) \ + FIELD(HadOriginalOpener, bool) \ + FIELD(IsPopupSpam, bool) \ + /* Hold the audio muted state and should be used on top level browsing \ + * contexts only */ \ + FIELD(Muted, bool) \ + /* Hold the pinned/app-tab state and should be used on top level browsing \ + * contexts only */ \ + FIELD(IsAppTab, bool) \ + /* Whether there's more than 1 tab / toplevel browsing context in this \ + * parent window. Used to determine if a given BC is allowed to resize \ + * and/or move the window or not. */ \ + FIELD(HasSiblings, bool) \ + /* Indicate that whether we should delay media playback, which would only \ + be done on an unvisited tab. And this should only be used on the top \ + level browsing contexts */ \ + FIELD(ShouldDelayMediaFromStart, bool) \ + /* See nsSandboxFlags.h for the possible flags. */ \ + FIELD(SandboxFlags, uint32_t) \ + /* The value of SandboxFlags when the BrowsingContext is first created. \ + * Used for sandboxing the initial about:blank document. */ \ + FIELD(InitialSandboxFlags, uint32_t) \ + /* A non-zero unique identifier for the browser element that is hosting \ + * this \ + * BrowsingContext tree. Every BrowsingContext in the element's tree will \ + * return the same ID in all processes and it will remain stable \ + * regardless of process changes. When a browser element's frameloader is \ + * switched to another browser element this ID will remain the same but \ + * hosted under the under the new browser element. */ \ + FIELD(BrowserId, uint64_t) \ + FIELD(HistoryID, nsID) \ + FIELD(InRDMPane, bool) \ + FIELD(Loading, bool) \ + /* A field only set on top browsing contexts, which indicates that either: \ + * \ + * * This is a browsing context created explicitly for printing or print \ + * preview (thus hosting static documents). \ + * \ + * * This is a browsing context where something in this tree is calling \ + * window.print() (and thus showing a modal dialog). \ + * \ + * We use it exclusively to block navigation for both of these cases. */ \ + FIELD(IsPrinting, bool) \ + FIELD(AncestorLoading, bool) \ + FIELD(AllowPlugins, bool) \ + FIELD(AllowContentRetargeting, bool) \ + FIELD(AllowContentRetargetingOnChildren, bool) \ + FIELD(ForceEnableTrackingProtection, bool) \ + FIELD(UseGlobalHistory, bool) \ + FIELD(TargetTopLevelLinkClicksToBlankInternal, bool) \ + FIELD(FullscreenAllowedByOwner, bool) \ + FIELD(ForceDesktopViewport, bool) \ + /* \ + * "is popup" in the spec. \ + * Set only on top browsing contexts. \ + * This doesn't indicate whether this is actually a popup or not, \ + * but whether this browsing context is created by requesting popup or not. \ + * See also: nsWindowWatcher::ShouldOpenPopup. \ + */ \ + FIELD(IsPopupRequested, bool) \ + /* These field are used to store the states of autoplay media request on \ + * GeckoView only, and it would only be modified on the top level browsing \ + * context. */ \ + FIELD(GVAudibleAutoplayRequestStatus, GVAutoplayRequestStatus) \ + FIELD(GVInaudibleAutoplayRequestStatus, GVAutoplayRequestStatus) \ + /* ScreenOrientation-related APIs */ \ + FIELD(CurrentOrientationAngle, float) \ + FIELD(CurrentOrientationType, mozilla::dom::OrientationType) \ + FIELD(OrientationLock, mozilla::hal::ScreenOrientation) \ + FIELD(UserAgentOverride, nsString) \ + FIELD(TouchEventsOverrideInternal, mozilla::dom::TouchEventsOverride) \ + FIELD(EmbedderElementType, Maybe<nsString>) \ + FIELD(MessageManagerGroup, nsString) \ + FIELD(MaxTouchPointsOverride, uint8_t) \ + FIELD(FullZoom, float) \ + FIELD(WatchedByDevToolsInternal, bool) \ + FIELD(TextZoom, float) \ + FIELD(OverrideDPPX, float) \ + /* The current in-progress load. */ \ + FIELD(CurrentLoadIdentifier, Maybe<uint64_t>) \ + /* See nsIRequest for possible flags. */ \ + FIELD(DefaultLoadFlags, uint32_t) \ + /* Signals that session history is enabled for this browsing context tree. \ + * This is only ever set to true on the top BC, so consumers need to get \ + * the value from the top BC! */ \ + FIELD(HasSessionHistory, bool) \ + /* Tracks if this context is the only top-level document in the session \ + * history of the context. */ \ + FIELD(IsSingleToplevelInHistory, bool) \ + FIELD(UseErrorPages, bool) \ + FIELD(PlatformOverride, nsString) \ + /* Specifies if this BC has loaded documents besides the initial \ + * about:blank document. about:privatebrowsing, about:home, about:newtab \ + * and non-initial about:blank are not considered to be initial \ + * documents. */ \ + FIELD(HasLoadedNonInitialDocument, bool) \ + /* Default value for nsIContentViewer::authorStyleDisabled in any new \ + * browsing contexts created as a descendant of this one. Valid only for \ + * top BCs. */ \ + FIELD(AuthorStyleDisabledDefault, bool) \ + FIELD(ServiceWorkersTestingEnabled, bool) \ + FIELD(MediumOverride, nsString) \ + /* DevTools override for prefers-color-scheme */ \ + FIELD(PrefersColorSchemeOverride, dom::PrefersColorSchemeOverride) \ + /* prefers-color-scheme override based on the color-scheme style of our \ + * <browser> embedder element. */ \ + FIELD(EmbedderColorSchemes, EmbedderColorSchemes) \ + FIELD(DisplayMode, dom::DisplayMode) \ + /* The number of entries added to the session history because of this \ + * browsing context. */ \ + FIELD(HistoryEntryCount, uint32_t) \ + /* Don't use the getter of the field, but IsInBFCache() method */ \ + FIELD(IsInBFCache, bool) \ + FIELD(HasRestoreData, bool) \ + FIELD(SessionStoreEpoch, uint32_t) \ + /* Whether we can execute scripts in this BrowsingContext. Has no effect \ + * unless scripts are also allowed in the parent WindowContext. */ \ + FIELD(AllowJavascript, bool) \ + /* The count of request that are used to prevent the browsing context tree \ + * from being suspended, which would ONLY be modified on the top level \ + * context in the chrome process because that's a non-atomic counter */ \ + FIELD(PageAwakeRequestCount, uint32_t) \ + /* This field only gets incrememented when we start navigations in the \ + * parent process. This is used for keeping track of the racing navigations \ + * between the parent and content processes. */ \ + FIELD(ParentInitiatedNavigationEpoch, uint64_t) \ + /* This browsing context is for a synthetic image document wrapping an \ + * image embedded in <object> or <embed>. */ \ + FIELD(SyntheticDocumentContainer, bool) \ + /* If true, this document is embedded within a content document, either \ + * loaded in the parent (e.g. about:addons or the devtools toolbox), or in \ + * a content process. */ \ + FIELD(EmbeddedInContentDocument, bool) \ + /* If true, this browsing context is within a hidden embedded document. */ \ + FIELD(IsUnderHiddenEmbedderElement, bool) + +// BrowsingContext, in this context, is the cross process replicated +// environment in which information about documents is stored. In +// particular the tree structure of nested browsing contexts is +// represented by the tree of BrowsingContexts. +// +// The tree of BrowsingContexts is created in step with its +// corresponding nsDocShell, and when nsDocShells are connected +// through a parent/child relationship, so are BrowsingContexts. The +// major difference is that BrowsingContexts are replicated (synced) +// to the parent process, making it possible to traverse the +// BrowsingContext tree for a tab, in both the parent and the child +// process. +// +// Trees of BrowsingContexts should only ever contain nodes of the +// same BrowsingContext::Type. This is enforced by asserts in the +// BrowsingContext::Create* methods. +class BrowsingContext : public nsILoadContext, public nsWrapperCache { + MOZ_DECL_SYNCED_CONTEXT(BrowsingContext, MOZ_EACH_BC_FIELD) + + public: + enum class Type { Chrome, Content }; + + static void Init(); + static LogModule* GetLog(); + static LogModule* GetSyncLog(); + + // Look up a BrowsingContext in the current process by ID. + static already_AddRefed<BrowsingContext> Get(uint64_t aId); + static already_AddRefed<BrowsingContext> Get(GlobalObject&, uint64_t aId) { + return Get(aId); + } + // Look up the top-level BrowsingContext by BrowserID. + static already_AddRefed<BrowsingContext> GetCurrentTopByBrowserId( + uint64_t aBrowserId); + static already_AddRefed<BrowsingContext> GetCurrentTopByBrowserId( + GlobalObject&, uint64_t aId) { + return GetCurrentTopByBrowserId(aId); + } + + static void UpdateCurrentTopByBrowserId(BrowsingContext* aNewBrowsingContext); + + static already_AddRefed<BrowsingContext> GetFromWindow( + WindowProxyHolder& aProxy); + static already_AddRefed<BrowsingContext> GetFromWindow( + GlobalObject&, WindowProxyHolder& aProxy) { + return GetFromWindow(aProxy); + } + + static void DiscardFromContentParent(ContentParent* aCP); + + // Create a brand-new toplevel BrowsingContext with no relationships to other + // BrowsingContexts, and which is not embedded within any <browser> or frame + // element. + // + // This BrowsingContext is immediately attached, and cannot have LoadContext + // flags customized unless it is of `Type::Chrome`. + // + // The process which created this BrowsingContext is responsible for detaching + // it. + static already_AddRefed<BrowsingContext> CreateIndependent(Type aType); + + // Create a brand-new BrowsingContext object, but does not immediately attach + // it. State such as OriginAttributes and PrivateBrowsingId may be customized + // to configure the BrowsingContext before it is attached. + // + // `EnsureAttached()` must be called before the BrowsingContext is used for a + // DocShell, BrowserParent, or BrowserBridgeChild. + static already_AddRefed<BrowsingContext> CreateDetached( + nsGlobalWindowInner* aParent, BrowsingContext* aOpener, + BrowsingContextGroup* aSpecificGroup, const nsAString& aName, Type aType, + bool aIsPopupRequested, bool aCreatedDynamically = false); + + void EnsureAttached(); + + bool EverAttached() const { return mEverAttached; } + + // Cast this object to a canonical browsing context, and return it. + CanonicalBrowsingContext* Canonical(); + + // Is the most recent Document in this BrowsingContext loaded within this + // process? This may be true with a null mDocShell after the Window has been + // closed. + bool IsInProcess() const { return mIsInProcess; } + + bool IsOwnedByProcess() const; + + bool CanHaveRemoteOuterProxies() const { + return !mIsInProcess || mDanglingRemoteOuterProxies; + } + + // Has this BrowsingContext been discarded. A discarded browsing context has + // been destroyed, and may not be available on the other side of an IPC + // message. + bool IsDiscarded() const { return mIsDiscarded; } + + // Returns true if none of the BrowsingContext's ancestor BrowsingContexts or + // WindowContexts are discarded or cached. + bool AncestorsAreCurrent() const; + + bool Windowless() const { return mWindowless; } + + // Get the DocShell for this BrowsingContext if it is in-process, or + // null if it's not. + nsIDocShell* GetDocShell() const { return mDocShell; } + void SetDocShell(nsIDocShell* aDocShell); + void ClearDocShell() { mDocShell = nullptr; } + + // Get the Document for this BrowsingContext if it is in-process, or + // null if it's not. + Document* GetDocument() const { + return mDocShell ? mDocShell->GetDocument() : nullptr; + } + Document* GetExtantDocument() const { + return mDocShell ? mDocShell->GetExtantDocument() : nullptr; + } + + // This cleans up remote outer window proxies that might have been left behind + // when the browsing context went from being remote to local. It does this by + // turning them into cross-compartment wrappers to aOuter. If there is already + // a remote proxy in the compartment of aOuter, then aOuter will get swapped + // to it and the value of aOuter will be set to the object that used to be the + // remote proxy and is now an OuterWindowProxy. + void CleanUpDanglingRemoteOuterWindowProxies( + JSContext* aCx, JS::MutableHandle<JSObject*> aOuter); + + // Get the embedder element for this BrowsingContext if the embedder is + // in-process, or null if it's not. + Element* GetEmbedderElement() const { return mEmbedderElement; } + void SetEmbedderElement(Element* aEmbedder); + + // Return true if the type of the embedder element is either object + // or embed, false otherwise. + bool IsEmbedderTypeObjectOrEmbed(); + + // Called after the BrowingContext has been embedded in a FrameLoader. This + // happens after `SetEmbedderElement` is called on the BrowsingContext and + // after the BrowsingContext has been set on the FrameLoader. + void Embed(); + + // Get the outer window object for this BrowsingContext if it is in-process + // and still has a docshell, or null otherwise. + nsPIDOMWindowOuter* GetDOMWindow() const { + return mDocShell ? mDocShell->GetWindow() : nullptr; + } + + uint64_t GetRequestContextId() const { return mRequestContextId; } + + // Detach the current BrowsingContext from its parent, in both the + // child and the parent process. + void Detach(bool aFromIPC = false); + + // Prepare this BrowsingContext to leave the current process. + void PrepareForProcessChange(); + + // Triggers a load in the process which currently owns this BrowsingContext. + nsresult LoadURI(nsDocShellLoadState* aLoadState, + bool aSetNavigating = false); + + nsresult InternalLoad(nsDocShellLoadState* aLoadState); + + // Removes the root document for this BrowsingContext tree from the BFCache, + // if it is cached, and returns true if it was. + bool RemoveRootFromBFCacheSync(); + + // If the load state includes a source BrowsingContext has been passed, check + // to see if we are sandboxed from it as the result of an iframe or CSP + // sandbox. + nsresult CheckSandboxFlags(nsDocShellLoadState* aLoadState); + + void DisplayLoadError(const nsAString& aURI); + + // Check that this browsing context is targetable for navigations (i.e. that + // it is neither closed, cached, nor discarded). + bool IsTargetable() const; + + // True if this browsing context is inactive and is able to be suspended. + bool InactiveForSuspend() const; + + const nsString& Name() const { return GetName(); } + void GetName(nsAString& aName) { aName = GetName(); } + bool NameEquals(const nsAString& aName) { return GetName().Equals(aName); } + + Type GetType() const { return mType; } + bool IsContent() const { return mType == Type::Content; } + bool IsChrome() const { return !IsContent(); } + + bool IsTop() const { return !GetParent(); } + bool IsSubframe() const { return !IsTop(); } + + bool IsTopContent() const { return IsContent() && IsTop(); } + + bool IsInSubtreeOf(BrowsingContext* aContext); + + bool IsContentSubframe() const { return IsContent() && IsSubframe(); } + + // non-zero + uint64_t Id() const { return mBrowsingContextId; } + + BrowsingContext* GetParent() const; + BrowsingContext* Top(); + const BrowsingContext* Top() const; + + int32_t IndexOf(BrowsingContext* aChild); + + // NOTE: Unlike `GetEmbedderWindowGlobal`, `GetParentWindowContext` does not + // cross toplevel content browser boundaries. + WindowContext* GetParentWindowContext() const { return mParentWindow; } + WindowContext* GetTopWindowContext() const; + + already_AddRefed<BrowsingContext> GetOpener() const { + RefPtr<BrowsingContext> opener(Get(GetOpenerId())); + if (!mIsDiscarded && opener && !opener->mIsDiscarded) { + MOZ_DIAGNOSTIC_ASSERT(opener->mType == mType); + return opener.forget(); + } + return nullptr; + } + void SetOpener(BrowsingContext* aOpener) { + MOZ_DIAGNOSTIC_ASSERT(!aOpener || aOpener->Group() == Group()); + MOZ_DIAGNOSTIC_ASSERT(!aOpener || aOpener->mType == mType); + + MOZ_ALWAYS_SUCCEEDS(SetOpenerId(aOpener ? aOpener->Id() : 0)); + } + + bool HasOpener() const; + + bool HadOriginalOpener() const { return GetHadOriginalOpener(); } + + // Returns true if the browsing context and top context are same origin + bool SameOriginWithTop(); + + /** + * When a new browsing context is opened by a sandboxed document, it needs to + * keep track of the browsing context that opened it, so that it can be + * navigated by it. This is the "one permitted sandboxed navigator". + */ + already_AddRefed<BrowsingContext> GetOnePermittedSandboxedNavigator() const { + return Get(GetOnePermittedSandboxedNavigatorId()); + } + [[nodiscard]] nsresult SetOnePermittedSandboxedNavigator( + BrowsingContext* aNavigator) { + if (GetOnePermittedSandboxedNavigatorId()) { + MOZ_ASSERT(false, + "One Permitted Sandboxed Navigator should only be set once."); + return NS_ERROR_FAILURE; + } else { + return SetOnePermittedSandboxedNavigatorId(aNavigator ? aNavigator->Id() + : 0); + } + } + + uint32_t SandboxFlags() const { return GetSandboxFlags(); } + + Span<RefPtr<BrowsingContext>> Children() const; + void GetChildren(nsTArray<RefPtr<BrowsingContext>>& aChildren); + + Span<RefPtr<BrowsingContext>> NonSyntheticChildren() const; + + const nsTArray<RefPtr<WindowContext>>& GetWindowContexts() { + return mWindowContexts; + } + void GetWindowContexts(nsTArray<RefPtr<WindowContext>>& aWindows); + + void RegisterWindowContext(WindowContext* aWindow); + void UnregisterWindowContext(WindowContext* aWindow); + WindowContext* GetCurrentWindowContext() const { + return mCurrentWindowContext; + } + + // Helpers to traverse this BrowsingContext subtree. Note that these will only + // traverse active contexts, and will ignore ones in the BFCache. + enum class WalkFlag { + Next, + Skip, + Stop, + }; + + /** + * Walk the browsing context tree in pre-order and call `aCallback` + * for every node in the tree. PreOrderWalk accepts two types of + * callbacks, either of the type `void(BrowsingContext*)` or + * `WalkFlag(BrowsingContext*)`. The former traverses the entire + * tree, but the latter let's you control if a sub-tree should be + * skipped by returning `WalkFlag::Skip`, completely abort traversal + * by returning `WalkFlag::Stop` or continue as normal with + * `WalkFlag::Next`. + */ + template <typename F> + void PreOrderWalk(F&& aCallback) { + if constexpr (std::is_void_v< + typename std::invoke_result_t<F, BrowsingContext*>>) { + PreOrderWalkVoid(std::forward<F>(aCallback)); + } else { + PreOrderWalkFlag(std::forward<F>(aCallback)); + } + } + + void PreOrderWalkVoid(const std::function<void(BrowsingContext*)>& aCallback); + WalkFlag PreOrderWalkFlag( + const std::function<WalkFlag(BrowsingContext*)>& aCallback); + + void PostOrderWalk(const std::function<void(BrowsingContext*)>& aCallback); + + void GetAllBrowsingContextsInSubtree( + nsTArray<RefPtr<BrowsingContext>>& aBrowsingContexts); + + BrowsingContextGroup* Group() { return mGroup; } + + // WebIDL bindings for nsILoadContext + Nullable<WindowProxyHolder> GetAssociatedWindow(); + Nullable<WindowProxyHolder> GetTopWindow(); + Element* GetTopFrameElement(); + bool GetIsContent() { return IsContent(); } + void SetUsePrivateBrowsing(bool aUsePrivateBrowsing, ErrorResult& aError); + // Needs a different name to disambiguate from the xpidl method with + // the same signature but different return value. + void SetUseTrackingProtectionWebIDL(bool aUseTrackingProtection, + ErrorResult& aRv); + bool UseTrackingProtectionWebIDL() { return UseTrackingProtection(); } + void GetOriginAttributes(JSContext* aCx, JS::MutableHandle<JS::Value> aVal, + ErrorResult& aError); + + bool InRDMPane() const { return GetInRDMPane(); } + + bool WatchedByDevTools(); + void SetWatchedByDevTools(bool aWatchedByDevTools, ErrorResult& aRv); + + dom::TouchEventsOverride TouchEventsOverride() const; + bool TargetTopLevelLinkClicksToBlank() const; + + bool FullscreenAllowed() const; + + float FullZoom() const { return GetFullZoom(); } + float TextZoom() const { return GetTextZoom(); } + + float OverrideDPPX() const { return Top()->GetOverrideDPPX(); } + + bool SuspendMediaWhenInactive() const { + return GetSuspendMediaWhenInactive(); + } + + bool IsActive() const; + void SetIsActive(bool aIsActive, mozilla::ErrorResult& aRv) { + SetExplicitActive(aIsActive ? ExplicitActiveStatus::Active + : ExplicitActiveStatus::Inactive, + aRv); + } + + bool ForceDesktopViewport() const { return GetForceDesktopViewport(); } + + bool AuthorStyleDisabledDefault() const { + return GetAuthorStyleDisabledDefault(); + } + + bool UseGlobalHistory() const { return GetUseGlobalHistory(); } + + bool GetIsActiveBrowserWindow(); + + void SetIsActiveBrowserWindow(bool aActive); + + uint64_t BrowserId() const { return GetBrowserId(); } + + bool IsLoading(); + + void GetEmbedderElementType(nsString& aElementType) { + if (GetEmbedderElementType().isSome()) { + aElementType = GetEmbedderElementType().value(); + } + } + + bool IsLoadingIdentifier(uint64_t aLoadIdentifer) { + if (GetCurrentLoadIdentifier() && + *GetCurrentLoadIdentifier() == aLoadIdentifer) { + return true; + } + return false; + } + + // ScreenOrientation related APIs + [[nodiscard]] nsresult SetCurrentOrientation(OrientationType aType, + float aAngle) { + Transaction txn; + txn.SetCurrentOrientationType(aType); + txn.SetCurrentOrientationAngle(aAngle); + return txn.Commit(this); + } + + void SetRDMPaneOrientation(OrientationType aType, float aAngle, + ErrorResult& aRv) { + if (InRDMPane()) { + if (NS_FAILED(SetCurrentOrientation(aType, aAngle))) { + aRv.ThrowInvalidStateError("Browsing context is discarded"); + } + } + } + + void SetRDMPaneMaxTouchPoints(uint8_t aMaxTouchPoints, ErrorResult& aRv) { + if (InRDMPane()) { + SetMaxTouchPointsOverride(aMaxTouchPoints, aRv); + } + } + + // Find a browsing context in this context's list of + // children. Doesn't consider the special names, '_self', '_parent', + // '_top', or '_blank'. Performs access control checks with regard to + // 'this'. + BrowsingContext* FindChildWithName(const nsAString& aName, + WindowGlobalChild& aRequestingWindow); + + // Find a browsing context in the subtree rooted at 'this' Doesn't + // consider the special names, '_self', '_parent', '_top', or + // '_blank'. + // + // If passed, performs access control checks with regard to + // 'aRequestingContext', otherwise performs no access checks. + BrowsingContext* FindWithNameInSubtree(const nsAString& aName, + WindowGlobalChild* aRequestingWindow); + + // Find the special browsing context if aName is '_self', '_parent', + // '_top', but not '_blank'. The latter is handled in FindWithName + BrowsingContext* FindWithSpecialName(const nsAString& aName, + WindowGlobalChild& aRequestingWindow); + + nsISupports* GetParentObject() const; + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Return the window proxy object that corresponds to this browsing context. + inline JSObject* GetWindowProxy() const { return mWindowProxy; } + inline JSObject* GetUnbarrieredWindowProxy() const { + return mWindowProxy.unbarrieredGet(); + } + + // Set the window proxy object that corresponds to this browsing context. + void SetWindowProxy(JS::Handle<JSObject*> aWindowProxy) { + mWindowProxy = aWindowProxy; + } + + Nullable<WindowProxyHolder> GetWindow(); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SKIPPABLE_SCRIPT_HOLDER_CLASS(BrowsingContext) + NS_DECL_NSILOADCONTEXT + + // Window APIs that are cross-origin-accessible (from the HTML spec). + WindowProxyHolder Window(); + BrowsingContext* GetBrowsingContext() { return this; }; + BrowsingContext* Self() { return this; } + void Location(JSContext* aCx, JS::MutableHandle<JSObject*> aLocation, + ErrorResult& aError); + void Close(CallerType aCallerType, ErrorResult& aError); + bool GetClosed(ErrorResult&) { return GetClosed(); } + void Focus(CallerType aCallerType, ErrorResult& aError); + void Blur(CallerType aCallerType, ErrorResult& aError); + WindowProxyHolder GetFrames(ErrorResult& aError); + int32_t Length() const { return Children().Length(); } + Nullable<WindowProxyHolder> GetTop(ErrorResult& aError); + void GetOpener(JSContext* aCx, JS::MutableHandle<JS::Value> aOpener, + ErrorResult& aError) const; + Nullable<WindowProxyHolder> GetParent(ErrorResult& aError); + void PostMessageMoz(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const nsAString& aTargetOrigin, + const Sequence<JSObject*>& aTransfer, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aError); + void PostMessageMoz(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const WindowPostMessageOptions& aOptions, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aError); + + void GetCustomUserAgent(nsAString& aUserAgent) { + aUserAgent = Top()->GetUserAgentOverride(); + } + nsresult SetCustomUserAgent(const nsAString& aUserAgent); + void SetCustomUserAgent(const nsAString& aUserAgent, ErrorResult& aRv); + + void GetCustomPlatform(nsAString& aPlatform) { + aPlatform = Top()->GetPlatformOverride(); + } + void SetCustomPlatform(const nsAString& aPlatform, ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx); + + static JSObject* ReadStructuredClone(JSContext* aCx, + JSStructuredCloneReader* aReader, + StructuredCloneHolder* aHolder); + bool WriteStructuredClone(JSContext* aCx, JSStructuredCloneWriter* aWriter, + StructuredCloneHolder* aHolder); + + void StartDelayedAutoplayMediaComponents(); + + [[nodiscard]] nsresult ResetGVAutoplayRequestStatus(); + + /** + * Information required to initialize a BrowsingContext in another process. + * This object may be serialized over IPC. + */ + struct IPCInitializer { + uint64_t mId = 0; + + // IDs are used for Parent and Opener to allow for this object to be + // deserialized before other BrowsingContext in the BrowsingContextGroup + // have been initialized. + uint64_t mParentId = 0; + already_AddRefed<WindowContext> GetParent(); + already_AddRefed<BrowsingContext> GetOpener(); + + uint64_t GetOpenerId() const { return mFields.Get<IDX_OpenerId>(); } + + bool mWindowless = false; + bool mUseRemoteTabs = false; + bool mUseRemoteSubframes = false; + bool mCreatedDynamically = false; + int32_t mChildOffset = 0; + int32_t mSessionHistoryIndex = -1; + int32_t mSessionHistoryCount = 0; + OriginAttributes mOriginAttributes; + uint64_t mRequestContextId = 0; + + FieldValues mFields; + }; + + // Create an IPCInitializer object for this BrowsingContext. + IPCInitializer GetIPCInitializer(); + + // Create a BrowsingContext object from over IPC. + static mozilla::ipc::IPCResult CreateFromIPC(IPCInitializer&& aInitializer, + BrowsingContextGroup* aGroup, + ContentParent* aOriginProcess); + + bool IsSandboxedFrom(BrowsingContext* aTarget); + + // The runnable will be called once there is idle time, or the top level + // page has been loaded or if a timeout has fired. + // Must be called only on the top level BrowsingContext. + void AddDeprioritizedLoadRunner(nsIRunnable* aRunner); + + RefPtr<SessionStorageManager> GetSessionStorageManager(); + + // Set PendingInitialization on this BrowsingContext before the context has + // been attached. + void InitPendingInitialization(bool aPendingInitialization) { + MOZ_ASSERT(!EverAttached()); + mFields.SetWithoutSyncing<IDX_PendingInitialization>( + aPendingInitialization); + } + + bool CreatedDynamically() const { return mCreatedDynamically; } + + // Returns true if this browsing context, or any ancestor to this browsing + // context was created dynamically. See also `CreatedDynamically`. + bool IsDynamic() const; + + int32_t ChildOffset() const { return mChildOffset; } + + bool GetOffsetPath(nsTArray<uint32_t>& aPath) const; + + const OriginAttributes& OriginAttributesRef() { return mOriginAttributes; } + nsresult SetOriginAttributes(const OriginAttributes& aAttrs); + + void GetHistoryID(JSContext* aCx, JS::MutableHandle<JS::Value> aVal, + ErrorResult& aError); + + // This should only be called on the top browsing context. + void InitSessionHistory(); + + // This will only ever return a non-null value if called on the top browsing + // context. + ChildSHistory* GetChildSessionHistory(); + + bool CrossOriginIsolated(); + + // Check if it is allowed to open a popup from the current browsing + // context or any of its ancestors. + bool IsPopupAllowed(); + + // aCurrentURI is only required to be non-null if the load type contains the + // nsIWebNavigation::LOAD_FLAGS_IS_REFRESH flag and aInfo is for a refresh to + // the current URI. + void SessionHistoryCommit(const LoadingSessionHistoryInfo& aInfo, + uint32_t aLoadType, nsIURI* aCurrentURI, + SessionHistoryInfo* aPreviousActiveEntry, + bool aPersist, bool aCloneEntryChildren, + bool aChannelExpired, uint32_t aCacheKey); + + // Set a new active entry on this browsing context. This is used for + // implementing history.pushState/replaceState and same document navigations. + // The new active entry will be linked to the current active entry through + // its shared state. + // aPreviousScrollPos is the scroll position that needs to be saved on the + // previous active entry. + // aUpdatedCacheKey is the cache key to set on the new active entry. If + // aUpdatedCacheKey is 0 then it will be ignored. + void SetActiveSessionHistoryEntry(const Maybe<nsPoint>& aPreviousScrollPos, + SessionHistoryInfo* aInfo, + uint32_t aLoadType, + uint32_t aUpdatedCacheKey, + bool aUpdateLength = true); + + // Replace the active entry for this browsing context. This is used for + // implementing history.replaceState and same document navigations. + void ReplaceActiveSessionHistoryEntry(SessionHistoryInfo* aInfo); + + // Removes dynamic child entries of the active entry. + void RemoveDynEntriesFromActiveSessionHistoryEntry(); + + // Removes entries corresponding to this BrowsingContext from session history. + void RemoveFromSessionHistory(const nsID& aChangeID); + + void SetTriggeringAndInheritPrincipals(nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + uint64_t aLoadIdentifier); + + // Return mTriggeringPrincipal and mPrincipalToInherit if the load id + // saved with the principal matches the current load identifier of this BC. + std::tuple<nsCOMPtr<nsIPrincipal>, nsCOMPtr<nsIPrincipal>> + GetTriggeringAndInheritPrincipalsForCurrentLoad(); + + void HistoryGo(int32_t aOffset, uint64_t aHistoryEpoch, + bool aRequireUserInteraction, bool aUserActivation, + std::function<void(Maybe<int32_t>&&)>&& aResolver); + + bool ShouldUpdateSessionHistory(uint32_t aLoadType); + + // Checks if we reached the rate limit for calls to Location and History API. + // The rate limit is controlled by the + // "dom.navigation.locationChangeRateLimit" prefs. + // Rate limit applies per BrowsingContext. + // Returns NS_OK if we are below the rate limit and increments the counter. + // Returns NS_ERROR_DOM_SECURITY_ERR if limit is reached. + nsresult CheckLocationChangeRateLimit(CallerType aCallerType); + + void ResetLocationChangeRateLimit(); + + mozilla::dom::DisplayMode DisplayMode() { return Top()->GetDisplayMode(); } + + // Returns canFocus, isActive + std::tuple<bool, bool> CanFocusCheck(CallerType aCallerType); + + bool CanBlurCheck(CallerType aCallerType); + + PopupBlocker::PopupControlState RevisePopupAbuseLevel( + PopupBlocker::PopupControlState aControl); + + void IncrementHistoryEntryCountForBrowsingContext(); + + bool ServiceWorkersTestingEnabled() const { + return GetServiceWorkersTestingEnabled(); + } + + void GetMediumOverride(nsAString& aOverride) const { + aOverride = GetMediumOverride(); + } + + dom::PrefersColorSchemeOverride PrefersColorSchemeOverride() const { + return GetPrefersColorSchemeOverride(); + } + + bool IsInBFCache() const; + + bool AllowJavascript() const { return GetAllowJavascript(); } + bool CanExecuteScripts() const { return mCanExecuteScripts; } + + uint32_t DefaultLoadFlags() const { return GetDefaultLoadFlags(); } + + // When request for page awake, it would increase a count that is used to + // prevent whole browsing context tree from being suspended. The request can + // be called multiple times. When calling the revoke, it would decrease the + // count and once the count reaches to zero, the browsing context tree could + // be suspended when the tree is inactive. + void RequestForPageAwake(); + void RevokeForPageAwake(); + + void AddDiscardListener(std::function<void(uint64_t)>&& aListener); + + bool IsAppTab() { return GetIsAppTab(); } + bool HasSiblings() { return GetHasSiblings(); } + + bool IsUnderHiddenEmbedderElement() const { + return GetIsUnderHiddenEmbedderElement(); + } + + protected: + virtual ~BrowsingContext(); + BrowsingContext(WindowContext* aParentWindow, BrowsingContextGroup* aGroup, + uint64_t aBrowsingContextId, Type aType, FieldValues&& aInit); + + void SetChildSHistory(ChildSHistory* aChildSHistory); + already_AddRefed<ChildSHistory> ForgetChildSHistory() { + // FIXME Do we need to unset mHasSessionHistory? + return mChildSessionHistory.forget(); + } + + static bool ShouldAddEntryForRefresh(nsIURI* aCurrentURI, + const SessionHistoryInfo& aInfo); + static bool ShouldAddEntryForRefresh(nsIURI* aCurrentURI, nsIURI* aNewURI, + bool aHasPostData); + + private: + mozilla::ipc::IPCResult Attach(bool aFromIPC, ContentParent* aOriginProcess); + + // Recomputes whether we can execute scripts in this BrowsingContext based on + // the value of AllowJavascript() and whether scripts are allowed in the + // parent WindowContext. Called whenever the AllowJavascript() flag or the + // parent WC changes. + void RecomputeCanExecuteScripts(); + + // Is it early enough in the BrowsingContext's lifecycle that it is still + // OK to set OriginAttributes? + bool CanSetOriginAttributes(); + + void AssertOriginAttributesMatchPrivateBrowsing(); + + // Assert that the BrowsingContext's LoadContext flags appear coherent + // relative to related BrowsingContexts. + void AssertCoherentLoadContext(); + + friend class ::nsOuterWindowProxy; + friend class ::nsGlobalWindowOuter; + friend class WindowContext; + + // Update the window proxy object that corresponds to this browsing context. + // This should be called from the window proxy object's objectMoved hook, if + // the object mWindowProxy points to was moved by the JS GC. + void UpdateWindowProxy(JSObject* obj, JSObject* old) { + if (mWindowProxy) { + MOZ_ASSERT(mWindowProxy == old); + mWindowProxy = obj; + } + } + // Clear the window proxy object that corresponds to this browsing context. + // This should be called if the window proxy object is finalized, or it can't + // reach its browsing context anymore. + void ClearWindowProxy() { mWindowProxy = nullptr; } + + friend class Location; + friend class RemoteLocationProxy; + /** + * LocationProxy is the class for the native object stored as a private in a + * RemoteLocationProxy proxy representing a Location object in a different + * process. It forwards all operations to its BrowsingContext and aggregates + * its refcount to that BrowsingContext. + */ + class LocationProxy final : public LocationBase { + public: + MozExternalRefCountType AddRef() { return GetBrowsingContext()->AddRef(); } + MozExternalRefCountType Release() { + return GetBrowsingContext()->Release(); + } + + protected: + friend class RemoteLocationProxy; + BrowsingContext* GetBrowsingContext() override { + return reinterpret_cast<BrowsingContext*>( + uintptr_t(this) - offsetof(BrowsingContext, mLocation)); + } + + already_AddRefed<nsIDocShell> GetDocShell() override { return nullptr; } + }; + + // Send a given `BaseTransaction` object to the correct remote. + void SendCommitTransaction(ContentParent* aParent, + const BaseTransaction& aTxn, uint64_t aEpoch); + void SendCommitTransaction(ContentChild* aChild, const BaseTransaction& aTxn, + uint64_t aEpoch); + + bool CanSet(FieldIndex<IDX_SessionStoreEpoch>, uint32_t aEpoch, + ContentParent* aSource) { + return IsTop() && !aSource; + } + + void DidSet(FieldIndex<IDX_SessionStoreEpoch>, uint32_t aOldValue); + + using CanSetResult = syncedcontext::CanSetResult; + + // Ensure that opener is in the same BrowsingContextGroup. + bool CanSet(FieldIndex<IDX_OpenerId>, const uint64_t& aValue, + ContentParent* aSource) { + if (aValue != 0) { + RefPtr<BrowsingContext> opener = Get(aValue); + return opener && opener->Group() == Group(); + } + return true; + } + + bool CanSet(FieldIndex<IDX_OpenerPolicy>, + nsILoadInfo::CrossOriginOpenerPolicy, ContentParent*); + + bool CanSet(FieldIndex<IDX_ServiceWorkersTestingEnabled>, bool, + ContentParent*) { + return IsTop(); + } + + bool CanSet(FieldIndex<IDX_MediumOverride>, const nsString&, ContentParent*) { + return IsTop(); + } + + bool CanSet(FieldIndex<IDX_EmbedderColorSchemes>, const EmbedderColorSchemes&, + ContentParent* aSource) { + return CheckOnlyEmbedderCanSet(aSource); + } + + bool CanSet(FieldIndex<IDX_PrefersColorSchemeOverride>, + dom::PrefersColorSchemeOverride, ContentParent*) { + return IsTop(); + } + + void DidSet(FieldIndex<IDX_InRDMPane>, bool aOldValue); + + void DidSet(FieldIndex<IDX_EmbedderColorSchemes>, + EmbedderColorSchemes&& aOldValue); + + void DidSet(FieldIndex<IDX_PrefersColorSchemeOverride>, + dom::PrefersColorSchemeOverride aOldValue); + + template <typename Callback> + void WalkPresContexts(Callback&&); + void PresContextAffectingFieldChanged(); + + void DidSet(FieldIndex<IDX_MediumOverride>, nsString&& aOldValue); + + bool CanSet(FieldIndex<IDX_SuspendMediaWhenInactive>, bool, ContentParent*) { + return IsTop(); + } + + bool CanSet(FieldIndex<IDX_TouchEventsOverrideInternal>, + dom::TouchEventsOverride aTouchEventsOverride, + ContentParent* aSource); + void DidSet(FieldIndex<IDX_TouchEventsOverrideInternal>, + dom::TouchEventsOverride&& aOldValue); + + bool CanSet(FieldIndex<IDX_DisplayMode>, const enum DisplayMode& aDisplayMode, + ContentParent* aSource) { + return IsTop(); + } + + void DidSet(FieldIndex<IDX_DisplayMode>, enum DisplayMode aOldValue); + + void DidSet(FieldIndex<IDX_ExplicitActive>, ExplicitActiveStatus aOldValue); + + bool CanSet(FieldIndex<IDX_IsActiveBrowserWindowInternal>, const bool& aValue, + ContentParent* aSource); + void DidSet(FieldIndex<IDX_IsActiveBrowserWindowInternal>, bool aOldValue); + + // Ensure that we only set the flag on the top level browsingContext. + // And then, we do a pre-order walk in the tree to refresh the + // volume of all media elements. + void DidSet(FieldIndex<IDX_Muted>); + + bool CanSet(FieldIndex<IDX_IsAppTab>, const bool& aValue, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_HasSiblings>, const bool& aValue, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_ShouldDelayMediaFromStart>, const bool& aValue, + ContentParent* aSource); + void DidSet(FieldIndex<IDX_ShouldDelayMediaFromStart>, bool aOldValue); + + bool CanSet(FieldIndex<IDX_OverrideDPPX>, const float& aValue, + ContentParent* aSource); + void DidSet(FieldIndex<IDX_OverrideDPPX>, float aOldValue); + + bool CanSet(FieldIndex<IDX_EmbedderInnerWindowId>, const uint64_t& aValue, + ContentParent* aSource); + + CanSetResult CanSet(FieldIndex<IDX_CurrentInnerWindowId>, + const uint64_t& aValue, ContentParent* aSource); + + void DidSet(FieldIndex<IDX_CurrentInnerWindowId>); + + bool CanSet(FieldIndex<IDX_ParentInitiatedNavigationEpoch>, + const uint64_t& aValue, ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_IsPopupSpam>, const bool& aValue, + ContentParent* aSource); + + void DidSet(FieldIndex<IDX_IsPopupSpam>); + + void DidSet(FieldIndex<IDX_GVAudibleAutoplayRequestStatus>); + void DidSet(FieldIndex<IDX_GVInaudibleAutoplayRequestStatus>); + + void DidSet(FieldIndex<IDX_Loading>); + + void DidSet(FieldIndex<IDX_AncestorLoading>); + + void DidSet(FieldIndex<IDX_PlatformOverride>); + CanSetResult CanSet(FieldIndex<IDX_PlatformOverride>, + const nsString& aPlatformOverride, + ContentParent* aSource); + + void DidSet(FieldIndex<IDX_UserAgentOverride>); + CanSetResult CanSet(FieldIndex<IDX_UserAgentOverride>, + const nsString& aUserAgent, ContentParent* aSource); + bool CanSet(FieldIndex<IDX_OrientationLock>, + const mozilla::hal::ScreenOrientation& aOrientationLock, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_EmbedderElementType>, + const Maybe<nsString>& aInitiatorType, ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_MessageManagerGroup>, + const nsString& aMessageManagerGroup, ContentParent* aSource); + + CanSetResult CanSet(FieldIndex<IDX_AllowContentRetargeting>, + const bool& aAllowContentRetargeting, + ContentParent* aSource); + CanSetResult CanSet(FieldIndex<IDX_AllowContentRetargetingOnChildren>, + const bool& aAllowContentRetargetingOnChildren, + ContentParent* aSource); + CanSetResult CanSet(FieldIndex<IDX_AllowPlugins>, const bool& aAllowPlugins, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_FullscreenAllowedByOwner>, const bool&, + ContentParent*); + bool CanSet(FieldIndex<IDX_WatchedByDevToolsInternal>, + const bool& aWatchedByDevToolsInternal, ContentParent* aSource); + + CanSetResult CanSet(FieldIndex<IDX_DefaultLoadFlags>, + const uint32_t& aDefaultLoadFlags, + ContentParent* aSource); + void DidSet(FieldIndex<IDX_DefaultLoadFlags>); + + bool CanSet(FieldIndex<IDX_UseGlobalHistory>, const bool& aUseGlobalHistory, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_TargetTopLevelLinkClicksToBlankInternal>, + const bool& aTargetTopLevelLinkClicksToBlankInternal, + ContentParent* aSource); + + void DidSet(FieldIndex<IDX_HasSessionHistory>, bool aOldValue); + + bool CanSet(FieldIndex<IDX_BrowserId>, const uint32_t& aValue, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_UseErrorPages>, const bool& aUseErrorPages, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_PendingInitialization>, bool aNewValue, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_PageAwakeRequestCount>, uint32_t aNewValue, + ContentParent* aSource); + void DidSet(FieldIndex<IDX_PageAwakeRequestCount>, uint32_t aOldValue); + + CanSetResult CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue, + ContentParent* aSource); + void DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue); + + bool CanSet(FieldIndex<IDX_ForceDesktopViewport>, bool aValue, + ContentParent* aSource) { + return IsTop() && XRE_IsParentProcess(); + } + + // TODO(emilio): Maybe handle the flag being set dynamically without + // navigating? The previous code didn't do it tho, and a reload is probably + // worth it regardless. + // void DidSet(FieldIndex<IDX_ForceDesktopViewport>, bool aOldValue); + + bool CanSet(FieldIndex<IDX_HasRestoreData>, bool aNewValue, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>, + const bool& aIsUnderHiddenEmbedderElement, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_EmbeddedInContentDocument>, bool, + ContentParent* aSource) { + return CheckOnlyEmbedderCanSet(aSource); + } + + template <size_t I, typename T> + bool CanSet(FieldIndex<I>, const T&, ContentParent*) { + return true; + } + + // Overload `DidSet` to get notifications for a particular field being set. + // + // You can also overload the variant that gets the old value if you need it. + template <size_t I> + void DidSet(FieldIndex<I>) {} + template <size_t I, typename T> + void DidSet(FieldIndex<I>, T&& aOldValue) {} + + void DidSet(FieldIndex<IDX_FullZoom>, float aOldValue); + void DidSet(FieldIndex<IDX_TextZoom>, float aOldValue); + void DidSet(FieldIndex<IDX_AuthorStyleDisabledDefault>); + + bool CanSet(FieldIndex<IDX_IsInBFCache>, bool, ContentParent* aSource); + void DidSet(FieldIndex<IDX_IsInBFCache>); + + void DidSet(FieldIndex<IDX_SyntheticDocumentContainer>); + + void DidSet(FieldIndex<IDX_IsUnderHiddenEmbedderElement>, bool aOldValue); + + // Allow if the process attemping to set field is the same as the owning + // process. Deprecated. New code that might use this should generally be moved + // to WindowContext or be settable only by the parent process. + CanSetResult LegacyRevertIfNotOwningOrParentProcess(ContentParent* aSource); + + // True if the process attempting to set field is the same as the embedder's + // process. + bool CheckOnlyEmbedderCanSet(ContentParent* aSource); + + void CreateChildSHistory(); + + using PrincipalWithLoadIdentifierTuple = + std::tuple<nsCOMPtr<nsIPrincipal>, uint64_t>; + + nsIPrincipal* GetSavedPrincipal( + Maybe<PrincipalWithLoadIdentifierTuple> aPrincipalTuple); + + // Type of BrowsingContent + const Type mType; + + // Unique id identifying BrowsingContext + const uint64_t mBrowsingContextId; + + RefPtr<BrowsingContextGroup> mGroup; + RefPtr<WindowContext> mParentWindow; + nsCOMPtr<nsIDocShell> mDocShell; + + RefPtr<Element> mEmbedderElement; + + nsTArray<RefPtr<WindowContext>> mWindowContexts; + RefPtr<WindowContext> mCurrentWindowContext; + + // This is not a strong reference, but using a JS::Heap for that should be + // fine. The JSObject stored in here should be a proxy with a + // nsOuterWindowProxy handler, which will update the pointer from its + // objectMoved hook and clear it from its finalize hook. + JS::Heap<JSObject*> mWindowProxy; + LocationProxy mLocation; + + // OriginAttributes for this BrowsingContext. May not be changed after this + // BrowsingContext is attached. + OriginAttributes mOriginAttributes; + + // The network request context id, representing the nsIRequestContext + // associated with this BrowsingContext, and LoadGroups created for it. + uint64_t mRequestContextId = 0; + + // Determines if private browsing should be used. May not be changed after + // this BrowsingContext is attached. This field matches mOriginAttributes in + // content Browsing Contexts. Currently treated as a binary value: 1 - in + // private mode, 0 - not private mode. + uint32_t mPrivateBrowsingId; + + // True if Attach() has been called on this BrowsingContext already. + bool mEverAttached : 1; + + // Is the most recent Document in this BrowsingContext loaded within this + // process? This may be true with a null mDocShell after the Window has been + // closed. + bool mIsInProcess : 1; + + // Has this browsing context been discarded? BrowsingContexts should + // only be discarded once. + bool mIsDiscarded : 1; + + // True if this BrowsingContext has no associated visible window, and is owned + // by whichever process created it, even if top-level. + bool mWindowless : 1; + + // This is true if the BrowsingContext was out of process, but is now in + // process, and might have remote window proxies that need to be cleaned up. + bool mDanglingRemoteOuterProxies : 1; + + // True if this BrowsingContext has been embedded in a element in this + // process. + bool mEmbeddedByThisProcess : 1; + + // Determines if remote (out-of-process) tabs should be used. May not be + // changed after this BrowsingContext is attached. + bool mUseRemoteTabs : 1; + + // Determines if out-of-process iframes should be used. May not be changed + // after this BrowsingContext is attached. + bool mUseRemoteSubframes : 1; + + // True if this BrowsingContext is for a frame that was added dynamically. + bool mCreatedDynamically : 1; + + // Set to true if the browsing context is in the bfcache and pagehide has been + // dispatched. When coming out from the bfcache, the value is set to false + // before dispatching pageshow. + bool mIsInBFCache : 1; + + // Determines if we can execute scripts in this BrowsingContext. True if + // AllowJavascript() is true and script execution is allowed in the parent + // WindowContext. + bool mCanExecuteScripts : 1; + + // The original offset of this context in its container. This property is -1 + // if this BrowsingContext is for a frame that was added dynamically. + int32_t mChildOffset; + + // The start time of user gesture, this is only available if the browsing + // context is in process. + TimeStamp mUserGestureStart; + + // Triggering principal and principal to inherit need to point to original + // principal instances if the document is loaded in the same process as the + // process that initiated the load. When the load starts we save the + // principals along with the current load id. + // These principals correspond to the most recent load that took place within + // the process of this browsing context. + Maybe<PrincipalWithLoadIdentifierTuple> mTriggeringPrincipal; + Maybe<PrincipalWithLoadIdentifierTuple> mPrincipalToInherit; + + class DeprioritizedLoadRunner + : public mozilla::Runnable, + public mozilla::LinkedListElement<DeprioritizedLoadRunner> { + public: + explicit DeprioritizedLoadRunner(nsIRunnable* aInner) + : Runnable("DeprioritizedLoadRunner"), mInner(aInner) {} + + NS_IMETHOD Run() override { + if (mInner) { + RefPtr<nsIRunnable> inner = std::move(mInner); + inner->Run(); + } + + return NS_OK; + } + + private: + RefPtr<nsIRunnable> mInner; + }; + + mozilla::LinkedList<DeprioritizedLoadRunner> mDeprioritizedLoadRunner; + + RefPtr<SessionStorageManager> mSessionStorageManager; + RefPtr<ChildSHistory> mChildSessionHistory; + + nsTArray<std::function<void(uint64_t)>> mDiscardListeners; + + // Counter and time span for rate limiting Location and History API calls. + // Used by CheckLocationChangeRateLimit. Do not apply cross-process. + uint32_t mLocationChangeRateLimitCount; + mozilla::TimeStamp mLocationChangeRateLimitSpanStart; +}; + +/** + * Gets a WindowProxy object for a BrowsingContext that lives in a different + * process (creating the object if it doesn't already exist). The WindowProxy + * object will be in the compartment that aCx is currently in. This should only + * be called if aContext doesn't hold a docshell, otherwise the BrowsingContext + * lives in this process, and a same-process WindowProxy should be used (see + * nsGlobalWindowOuter). This should only be called by bindings code, ToJSValue + * is the right API to get a WindowProxy for a BrowsingContext. + * + * If aTransplantTo is non-null, then the WindowProxy object will eventually be + * transplanted onto it. Therefore it should be used as the value in the remote + * proxy map. + */ +extern bool GetRemoteOuterWindowProxy(JSContext* aCx, BrowsingContext* aContext, + JS::Handle<JSObject*> aTransplantTo, + JS::MutableHandle<JSObject*> aRetVal); + +using BrowsingContextTransaction = BrowsingContext::BaseTransaction; +using BrowsingContextInitializer = BrowsingContext::IPCInitializer; +using MaybeDiscardedBrowsingContext = MaybeDiscarded<BrowsingContext>; + +// Specialize the transaction object for every translation unit it's used in. +extern template class syncedcontext::Transaction<BrowsingContext>; + +} // namespace dom + +// Allow sending BrowsingContext objects over IPC. +namespace ipc { +template <> +struct IPDLParamTraits<dom::MaybeDiscarded<dom::BrowsingContext>> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::MaybeDiscarded<dom::BrowsingContext>& aParam); + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + dom::MaybeDiscarded<dom::BrowsingContext>* aResult); +}; + +template <> +struct IPDLParamTraits<dom::BrowsingContext::IPCInitializer> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::BrowsingContext::IPCInitializer& aInitializer); + + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + dom::BrowsingContext::IPCInitializer* aInitializer); +}; +} // namespace ipc +} // namespace mozilla + +#endif // !defined(mozilla_dom_BrowsingContext_h) diff --git a/docshell/base/BrowsingContextGroup.cpp b/docshell/base/BrowsingContextGroup.cpp new file mode 100644 index 0000000000..66ef235271 --- /dev/null +++ b/docshell/base/BrowsingContextGroup.cpp @@ -0,0 +1,579 @@ +/* -*- 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/. */ + +#include "mozilla/dom/BrowsingContextGroup.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/InputTaskManager.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/BrowsingContextBinding.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/ThrottledEventQueue.h" +#include "nsFocusManager.h" +#include "nsTHashMap.h" + +namespace mozilla { +namespace dom { + +// Maximum number of successive dialogs before we prompt users to disable +// dialogs for this window. +#define MAX_SUCCESSIVE_DIALOG_COUNT 5 + +static StaticRefPtr<BrowsingContextGroup> sChromeGroup; + +static StaticAutoPtr<nsTHashMap<uint64_t, RefPtr<BrowsingContextGroup>>> + sBrowsingContextGroups; + +already_AddRefed<BrowsingContextGroup> BrowsingContextGroup::GetOrCreate( + uint64_t aId) { + if (!sBrowsingContextGroups) { + sBrowsingContextGroups = + new nsTHashMap<nsUint64HashKey, RefPtr<BrowsingContextGroup>>(); + ClearOnShutdown(&sBrowsingContextGroups); + } + + return do_AddRef(sBrowsingContextGroups->LookupOrInsertWith( + aId, [&aId] { return do_AddRef(new BrowsingContextGroup(aId)); })); +} + +already_AddRefed<BrowsingContextGroup> BrowsingContextGroup::GetExisting( + uint64_t aId) { + if (sBrowsingContextGroups) { + return do_AddRef(sBrowsingContextGroups->Get(aId)); + } + return nullptr; +} + +// Only use 53 bits for the BrowsingContextGroup ID. +static constexpr uint64_t kBrowsingContextGroupIdTotalBits = 53; +static constexpr uint64_t kBrowsingContextGroupIdProcessBits = 22; +static constexpr uint64_t kBrowsingContextGroupIdFlagBits = 1; +static constexpr uint64_t kBrowsingContextGroupIdBits = + kBrowsingContextGroupIdTotalBits - kBrowsingContextGroupIdProcessBits - + kBrowsingContextGroupIdFlagBits; + +// IDs for the relevant flags +static constexpr uint64_t kPotentiallyCrossOriginIsolatedFlag = 0x1; + +// The next ID value which will be used. +static uint64_t sNextBrowsingContextGroupId = 1; + +// Generate the next ID with the given flags. +static uint64_t GenerateBrowsingContextGroupId(uint64_t aFlags) { + MOZ_RELEASE_ASSERT(aFlags < (uint64_t(1) << kBrowsingContextGroupIdFlagBits)); + uint64_t childId = XRE_IsContentProcess() + ? ContentChild::GetSingleton()->GetID() + : uint64_t(0); + MOZ_RELEASE_ASSERT(childId < + (uint64_t(1) << kBrowsingContextGroupIdProcessBits)); + uint64_t id = sNextBrowsingContextGroupId++; + MOZ_RELEASE_ASSERT(id < (uint64_t(1) << kBrowsingContextGroupIdBits)); + + return (childId << (kBrowsingContextGroupIdBits + + kBrowsingContextGroupIdFlagBits)) | + (id << kBrowsingContextGroupIdFlagBits) | aFlags; +} + +// Extract flags from the given ID. +static uint64_t GetBrowsingContextGroupIdFlags(uint64_t aId) { + return aId & ((uint64_t(1) << kBrowsingContextGroupIdFlagBits) - 1); +} + +uint64_t BrowsingContextGroup::CreateId(bool aPotentiallyCrossOriginIsolated) { + // We encode the potentially cross-origin isolated bit within the ID so that + // the information can be recovered whenever the group needs to be re-created + // due to e.g. being garbage-collected. + // + // In the future if we end up needing more complex information stored within + // the ID, we can consider converting it to a more complex type, like a + // string. + uint64_t flags = + aPotentiallyCrossOriginIsolated ? kPotentiallyCrossOriginIsolatedFlag : 0; + uint64_t id = GenerateBrowsingContextGroupId(flags); + MOZ_ASSERT(GetBrowsingContextGroupIdFlags(id) == flags); + return id; +} + +already_AddRefed<BrowsingContextGroup> BrowsingContextGroup::Create( + bool aPotentiallyCrossOriginIsolated) { + return GetOrCreate(CreateId(aPotentiallyCrossOriginIsolated)); +} + +BrowsingContextGroup::BrowsingContextGroup(uint64_t aId) : mId(aId) { + mTimerEventQueue = ThrottledEventQueue::Create( + GetMainThreadSerialEventTarget(), "BrowsingContextGroup timer queue"); + + mWorkerEventQueue = ThrottledEventQueue::Create( + GetMainThreadSerialEventTarget(), "BrowsingContextGroup worker queue"); +} + +void BrowsingContextGroup::Register(nsISupports* aContext) { + MOZ_DIAGNOSTIC_ASSERT(!mDestroyed); + MOZ_DIAGNOSTIC_ASSERT(aContext); + mContexts.Insert(aContext); +} + +void BrowsingContextGroup::Unregister(nsISupports* aContext) { + MOZ_DIAGNOSTIC_ASSERT(!mDestroyed); + MOZ_DIAGNOSTIC_ASSERT(aContext); + mContexts.Remove(aContext); + + MaybeDestroy(); +} + +void BrowsingContextGroup::EnsureHostProcess(ContentParent* aProcess) { + MOZ_DIAGNOSTIC_ASSERT(!mDestroyed); + MOZ_DIAGNOSTIC_ASSERT(this != sChromeGroup, + "cannot have content host for chrome group"); + MOZ_DIAGNOSTIC_ASSERT(aProcess->GetRemoteType() != PREALLOC_REMOTE_TYPE, + "cannot use preallocated process as host"); + MOZ_DIAGNOSTIC_ASSERT(!aProcess->GetRemoteType().IsEmpty(), + "host process must have remote type"); + + // XXX: The diagnostic crashes in bug 1816025 seemed to come through caller + // ContentParent::GetNewOrUsedLaunchingBrowserProcess where we already + // did AssertAlive, so IsDead should be irrelevant here. Still it reads + // wrong that we ever might do AddBrowsingContextGroup if aProcess->IsDead(). + if (aProcess->IsDead() || + mHosts.WithEntryHandle(aProcess->GetRemoteType(), [&](auto&& entry) { + if (entry) { + // We know from bug 1816025 that this happens quite often and we have + // bug 1815480 on file that should harden the entire flow. But in the + // meantime we can just live with NOT replacing the found host + // process with a new one here if it is still alive. + MOZ_ASSERT( + entry.Data() == aProcess, + "There's already another host process for this remote type"); + if (!entry.Data()->IsShuttingDown()) { + return false; + } + } + + // This process wasn't already marked as our host, so insert it (or + // update if the old process is shutting down), and begin subscribing, + // unless the process is still launching. + entry.InsertOrUpdate(do_AddRef(aProcess)); + + return true; + })) { + aProcess->AddBrowsingContextGroup(this); + } +} + +void BrowsingContextGroup::RemoveHostProcess(ContentParent* aProcess) { + MOZ_DIAGNOSTIC_ASSERT(aProcess); + MOZ_DIAGNOSTIC_ASSERT(aProcess->GetRemoteType() != PREALLOC_REMOTE_TYPE); + auto entry = mHosts.Lookup(aProcess->GetRemoteType()); + if (entry && entry.Data() == aProcess) { + entry.Remove(); + } +} + +static void CollectContextInitializers( + Span<RefPtr<BrowsingContext>> aContexts, + nsTArray<SyncedContextInitializer>& aInits) { + // The order that we record these initializers is important, as it will keep + // the order that children are attached to their parent in the newly connected + // content process consistent. + for (auto& context : aContexts) { + aInits.AppendElement(context->GetIPCInitializer()); + for (const auto& window : context->GetWindowContexts()) { + aInits.AppendElement(window->GetIPCInitializer()); + CollectContextInitializers(window->Children(), aInits); + } + } +} + +void BrowsingContextGroup::Subscribe(ContentParent* aProcess) { + MOZ_DIAGNOSTIC_ASSERT(!mDestroyed); + MOZ_DIAGNOSTIC_ASSERT(aProcess && !aProcess->IsLaunching()); + MOZ_DIAGNOSTIC_ASSERT(aProcess->GetRemoteType() != PREALLOC_REMOTE_TYPE); + + // Check if we're already subscribed to this process. + if (!mSubscribers.EnsureInserted(aProcess)) { + return; + } + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + // If the process is already marked as dead, we won't be the host, but may + // still need to subscribe to the process due to creating a popup while + // shutting down. + if (!aProcess->IsDead()) { + auto hostEntry = mHosts.Lookup(aProcess->GetRemoteType()); + MOZ_DIAGNOSTIC_ASSERT(hostEntry && hostEntry.Data() == aProcess, + "Cannot subscribe a non-host process"); + } +#endif + + // FIXME: This won't send non-discarded children of discarded BCs, but those + // BCs will be in the process of being destroyed anyway. + // FIXME: Prevent that situation from occuring. + nsTArray<SyncedContextInitializer> inits(mContexts.Count()); + CollectContextInitializers(mToplevels, inits); + + // Send all of our contexts to the target content process. + Unused << aProcess->SendRegisterBrowsingContextGroup(Id(), inits); + + // If the focused or active BrowsingContexts belong in this group, tell the + // newly subscribed process. + if (nsFocusManager* fm = nsFocusManager::GetFocusManager()) { + BrowsingContext* focused = fm->GetFocusedBrowsingContextInChrome(); + if (focused && focused->Group() != this) { + focused = nullptr; + } + BrowsingContext* active = fm->GetActiveBrowsingContextInChrome(); + if (active && active->Group() != this) { + active = nullptr; + } + + if (focused || active) { + Unused << aProcess->SendSetupFocusedAndActive( + focused, fm->GetActionIdForFocusedBrowsingContextInChrome(), active, + fm->GetActionIdForActiveBrowsingContextInChrome()); + } + } +} + +void BrowsingContextGroup::Unsubscribe(ContentParent* aProcess) { + MOZ_DIAGNOSTIC_ASSERT(aProcess); + MOZ_DIAGNOSTIC_ASSERT(aProcess->GetRemoteType() != PREALLOC_REMOTE_TYPE); + mSubscribers.Remove(aProcess); + aProcess->RemoveBrowsingContextGroup(this); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + auto hostEntry = mHosts.Lookup(aProcess->GetRemoteType()); + MOZ_DIAGNOSTIC_ASSERT(!hostEntry || hostEntry.Data() != aProcess, + "Unsubscribing existing host entry"); +#endif +} + +ContentParent* BrowsingContextGroup::GetHostProcess( + const nsACString& aRemoteType) { + return mHosts.GetWeak(aRemoteType); +} + +void BrowsingContextGroup::UpdateToplevelsSuspendedIfNeeded() { + if (!StaticPrefs::dom_suspend_inactive_enabled()) { + return; + } + + mToplevelsSuspended = ShouldSuspendAllTopLevelContexts(); + for (const auto& context : mToplevels) { + nsPIDOMWindowOuter* outer = context->GetDOMWindow(); + if (!outer) { + continue; + } + nsCOMPtr<nsPIDOMWindowInner> inner = outer->GetCurrentInnerWindow(); + if (!inner) { + continue; + } + if (mToplevelsSuspended && !inner->GetWasSuspendedByGroup()) { + inner->Suspend(); + inner->SetWasSuspendedByGroup(true); + } else if (!mToplevelsSuspended && inner->GetWasSuspendedByGroup()) { + inner->Resume(); + inner->SetWasSuspendedByGroup(false); + } + } +} + +bool BrowsingContextGroup::ShouldSuspendAllTopLevelContexts() const { + for (const auto& context : mToplevels) { + if (!context->InactiveForSuspend()) { + return false; + } + } + return true; +} + +BrowsingContextGroup::~BrowsingContextGroup() { Destroy(); } + +void BrowsingContextGroup::Destroy() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (mDestroyed) { + MOZ_DIAGNOSTIC_ASSERT(mHosts.Count() == 0); + MOZ_DIAGNOSTIC_ASSERT(mSubscribers.Count() == 0); + MOZ_DIAGNOSTIC_ASSERT_IF(sBrowsingContextGroups, + !sBrowsingContextGroups->Contains(Id()) || + *sBrowsingContextGroups->Lookup(Id()) != this); + } + mDestroyed = true; +#endif + + // Make sure to call `RemoveBrowsingContextGroup` for every entry in both + // `mHosts` and `mSubscribers`. This will visit most entries twice, but + // `RemoveBrowsingContextGroup` is safe to call multiple times. + for (const auto& entry : mHosts.Values()) { + entry->RemoveBrowsingContextGroup(this); + } + for (const auto& key : mSubscribers) { + key->RemoveBrowsingContextGroup(this); + } + mHosts.Clear(); + mSubscribers.Clear(); + + if (sBrowsingContextGroups) { + sBrowsingContextGroups->Remove(Id()); + } +} + +void BrowsingContextGroup::AddKeepAlive() { + MOZ_DIAGNOSTIC_ASSERT(!mDestroyed); + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + mKeepAliveCount++; +} + +void BrowsingContextGroup::RemoveKeepAlive() { + MOZ_DIAGNOSTIC_ASSERT(!mDestroyed); + MOZ_DIAGNOSTIC_ASSERT(mKeepAliveCount > 0); + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + mKeepAliveCount--; + + MaybeDestroy(); +} + +auto BrowsingContextGroup::MakeKeepAlivePtr() -> KeepAlivePtr { + AddKeepAlive(); + return KeepAlivePtr{do_AddRef(this).take()}; +} + +void BrowsingContextGroup::MaybeDestroy() { + // Once there are no synced contexts referencing a `BrowsingContextGroup`, we + // can clear subscribers and destroy this group. We only do this in the parent + // process, as it will orchestrate destruction of BCGs in content processes. + if (XRE_IsParentProcess() && mContexts.IsEmpty() && mKeepAliveCount == 0 && + this != sChromeGroup) { + Destroy(); + + // We may have been deleted here, as `Destroy()` will clear references. Do + // not access any members at this point. + } +} + +void BrowsingContextGroup::ChildDestroy() { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess()); + MOZ_DIAGNOSTIC_ASSERT(!mDestroyed); + MOZ_DIAGNOSTIC_ASSERT(mContexts.IsEmpty()); + Destroy(); +} + +nsISupports* BrowsingContextGroup::GetParentObject() const { + return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); +} + +JSObject* BrowsingContextGroup::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return BrowsingContextGroup_Binding::Wrap(aCx, this, aGivenProto); +} + +nsresult BrowsingContextGroup::QueuePostMessageEvent( + already_AddRefed<nsIRunnable>&& aRunnable) { + if (StaticPrefs::dom_separate_event_queue_for_post_message_enabled()) { + if (!mPostMessageEventQueue) { + nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget(); + mPostMessageEventQueue = ThrottledEventQueue::Create( + target, "PostMessage Queue", + nsIRunnablePriority::PRIORITY_DEFERRED_TIMERS); + nsresult rv = mPostMessageEventQueue->SetIsPaused(false); + MOZ_ALWAYS_SUCCEEDS(rv); + } + + // Ensure the queue is enabled + if (mPostMessageEventQueue->IsPaused()) { + nsresult rv = mPostMessageEventQueue->SetIsPaused(false); + MOZ_ALWAYS_SUCCEEDS(rv); + } + + if (mPostMessageEventQueue) { + mPostMessageEventQueue->Dispatch(std::move(aRunnable), + NS_DISPATCH_NORMAL); + return NS_OK; + } + } + return NS_ERROR_FAILURE; +} + +void BrowsingContextGroup::FlushPostMessageEvents() { + if (StaticPrefs::dom_separate_event_queue_for_post_message_enabled()) { + if (mPostMessageEventQueue) { + nsresult rv = mPostMessageEventQueue->SetIsPaused(true); + MOZ_ALWAYS_SUCCEEDS(rv); + nsCOMPtr<nsIRunnable> event; + while ((event = mPostMessageEventQueue->GetEvent())) { + NS_DispatchToMainThread(event.forget()); + } + } + } +} + +bool BrowsingContextGroup::HasActiveBC() { + for (auto& topLevelBC : Toplevels()) { + if (topLevelBC->IsActive()) { + return true; + } + } + return false; +} + +void BrowsingContextGroup::IncInputEventSuspensionLevel() { + MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled()); + if (!mHasIncreasedInputTaskManagerSuspensionLevel && HasActiveBC()) { + IncInputTaskManagerSuspensionLevel(); + } + ++mInputEventSuspensionLevel; +} + +void BrowsingContextGroup::DecInputEventSuspensionLevel() { + MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled()); + --mInputEventSuspensionLevel; + if (!mInputEventSuspensionLevel && + mHasIncreasedInputTaskManagerSuspensionLevel) { + DecInputTaskManagerSuspensionLevel(); + } +} + +void BrowsingContextGroup::DecInputTaskManagerSuspensionLevel() { + MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled()); + MOZ_ASSERT(mHasIncreasedInputTaskManagerSuspensionLevel); + + InputTaskManager::Get()->DecSuspensionLevel(); + mHasIncreasedInputTaskManagerSuspensionLevel = false; +} + +void BrowsingContextGroup::IncInputTaskManagerSuspensionLevel() { + MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled()); + MOZ_ASSERT(!mHasIncreasedInputTaskManagerSuspensionLevel); + MOZ_ASSERT(HasActiveBC()); + + InputTaskManager::Get()->IncSuspensionLevel(); + mHasIncreasedInputTaskManagerSuspensionLevel = true; +} + +void BrowsingContextGroup::UpdateInputTaskManagerIfNeeded(bool aIsActive) { + MOZ_ASSERT(StaticPrefs::dom_input_events_canSuspendInBCG_enabled()); + if (!aIsActive) { + if (mHasIncreasedInputTaskManagerSuspensionLevel) { + MOZ_ASSERT(mInputEventSuspensionLevel > 0); + if (!HasActiveBC()) { + DecInputTaskManagerSuspensionLevel(); + } + } + } else { + if (mInputEventSuspensionLevel && + !mHasIncreasedInputTaskManagerSuspensionLevel) { + IncInputTaskManagerSuspensionLevel(); + } + } +} + +/* static */ +BrowsingContextGroup* BrowsingContextGroup::GetChromeGroup() { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + if (!sChromeGroup && XRE_IsParentProcess()) { + sChromeGroup = BrowsingContextGroup::Create(); + ClearOnShutdown(&sChromeGroup); + } + + return sChromeGroup; +} + +void BrowsingContextGroup::GetDocGroups(nsTArray<DocGroup*>& aDocGroups) { + MOZ_ASSERT(NS_IsMainThread()); + AppendToArray(aDocGroups, mDocGroups.Values()); +} + +already_AddRefed<DocGroup> BrowsingContextGroup::AddDocument( + const nsACString& aKey, Document* aDocument) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<DocGroup>& docGroup = mDocGroups.LookupOrInsertWith( + aKey, [&] { return DocGroup::Create(this, aKey); }); + + docGroup->AddDocument(aDocument); + return do_AddRef(docGroup); +} + +void BrowsingContextGroup::RemoveDocument(Document* aDocument, + DocGroup* aDocGroup) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<DocGroup> docGroup = aDocGroup; + // Removing the last document in DocGroup might decrement the + // DocGroup BrowsingContextGroup's refcount to 0. + RefPtr<BrowsingContextGroup> kungFuDeathGrip(this); + docGroup->RemoveDocument(aDocument); + + if (docGroup->IsEmpty()) { + mDocGroups.Remove(docGroup->GetKey()); + } +} + +already_AddRefed<BrowsingContextGroup> BrowsingContextGroup::Select( + WindowContext* aParent, BrowsingContext* aOpener) { + if (aParent) { + return do_AddRef(aParent->Group()); + } + if (aOpener) { + return do_AddRef(aOpener->Group()); + } + return Create(); +} + +void BrowsingContextGroup::GetAllGroups( + nsTArray<RefPtr<BrowsingContextGroup>>& aGroups) { + aGroups.Clear(); + if (!sBrowsingContextGroups) { + return; + } + + aGroups = ToArray(sBrowsingContextGroups->Values()); +} + +// For tests only. +void BrowsingContextGroup::ResetDialogAbuseState() { + mDialogAbuseCount = 0; + // Reset the timer. + mLastDialogQuitTime = + TimeStamp::Now() - + TimeDuration::FromSeconds(DEFAULT_SUCCESSIVE_DIALOG_TIME_LIMIT); +} + +bool BrowsingContextGroup::DialogsAreBeingAbused() { + if (mLastDialogQuitTime.IsNull() || nsContentUtils::IsCallerChrome()) { + return false; + } + + TimeDuration dialogInterval(TimeStamp::Now() - mLastDialogQuitTime); + if (dialogInterval.ToSeconds() < + Preferences::GetInt("dom.successive_dialog_time_limit", + DEFAULT_SUCCESSIVE_DIALOG_TIME_LIMIT)) { + mDialogAbuseCount++; + + return PopupBlocker::GetPopupControlState() > PopupBlocker::openAllowed || + mDialogAbuseCount > MAX_SUCCESSIVE_DIALOG_COUNT; + } + + // Reset the abuse counter + mDialogAbuseCount = 0; + + return false; +} + +bool BrowsingContextGroup::IsPotentiallyCrossOriginIsolated() { + return GetBrowsingContextGroupIdFlags(mId) & + kPotentiallyCrossOriginIsolatedFlag; +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(BrowsingContextGroup, mContexts, + mToplevels, mHosts, mSubscribers, + mTimerEventQueue, mWorkerEventQueue, + mDocGroups) + +} // namespace dom +} // namespace mozilla diff --git a/docshell/base/BrowsingContextGroup.h b/docshell/base/BrowsingContextGroup.h new file mode 100644 index 0000000000..fb1f2e528c --- /dev/null +++ b/docshell/base/BrowsingContextGroup.h @@ -0,0 +1,318 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_BrowsingContextGroup_h +#define mozilla_dom_BrowsingContextGroup_h + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/FunctionRef.h" +#include "nsRefPtrHashtable.h" +#include "nsHashKeys.h" +#include "nsTArray.h" +#include "nsTHashSet.h" +#include "nsWrapperCache.h" +#include "nsXULAppAPI.h" + +namespace mozilla { +class ThrottledEventQueue; + +namespace dom { + +// Amount of time allowed between alert/prompt/confirm before enabling +// the stop dialog checkbox. +#define DEFAULT_SUCCESSIVE_DIALOG_TIME_LIMIT 3 // 3 sec + +class BrowsingContext; +class WindowContext; +class ContentParent; +class DocGroup; + +// A BrowsingContextGroup represents the Unit of Related Browsing Contexts in +// the standard. +// +// A BrowsingContext may not hold references to other BrowsingContext objects +// which are not in the same BrowsingContextGroup. +class BrowsingContextGroup final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BrowsingContextGroup) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(BrowsingContextGroup) + + // Interact with the list of synced contexts. This controls the lifecycle of + // the BrowsingContextGroup and contexts loaded within them. + void Register(nsISupports* aContext); + void Unregister(nsISupports* aContext); + + // Control which processes will be used to host documents loaded in this + // BrowsingContextGroup. There should only ever be one host process per remote + // type. + // + // A new host process will be subscribed to the BrowsingContextGroup unless it + // is still launching, in which case it will subscribe itself when it is done + // launching. + void EnsureHostProcess(ContentParent* aProcess); + + // A removed host process will no longer be used to host documents loaded in + // this BrowsingContextGroup. + void RemoveHostProcess(ContentParent* aProcess); + + // Synchronize the current BrowsingContextGroup state down to the given + // content process, and continue updating it. + // + // You rarely need to call this directly, as it's automatically called by + // |EnsureHostProcess| as needed. + void Subscribe(ContentParent* aProcess); + + // Stop synchronizing the current BrowsingContextGroup state down to a given + // content process. The content process must no longer be a host process. + void Unsubscribe(ContentParent* aProcess); + + // Look up the process which should be used to host documents with this + // RemoteType. This will be a non-dead process associated with this + // BrowsingContextGroup, if possible. + ContentParent* GetHostProcess(const nsACString& aRemoteType); + + // When a BrowsingContext is being discarded, we may want to keep the + // corresponding BrowsingContextGroup alive until the other process + // acknowledges that the BrowsingContext has been discarded. A `KeepAlive` + // will be added to the `BrowsingContextGroup`, delaying destruction. + void AddKeepAlive(); + void RemoveKeepAlive(); + + // A `KeepAlivePtr` will hold both a strong reference to the + // `BrowsingContextGroup` and holds a `KeepAlive`. When the pointer is + // dropped, it will release both the strong reference and the keepalive. + struct KeepAliveDeleter { + void operator()(BrowsingContextGroup* aPtr) { + if (RefPtr<BrowsingContextGroup> ptr = already_AddRefed(aPtr)) { + ptr->RemoveKeepAlive(); + } + } + }; + using KeepAlivePtr = UniquePtr<BrowsingContextGroup, KeepAliveDeleter>; + KeepAlivePtr MakeKeepAlivePtr(); + + // Call when we want to check if we should suspend or resume all top level + // contexts. + void UpdateToplevelsSuspendedIfNeeded(); + + // Get a reference to the list of toplevel contexts in this + // BrowsingContextGroup. + nsTArray<RefPtr<BrowsingContext>>& Toplevels() { return mToplevels; } + void GetToplevels(nsTArray<RefPtr<BrowsingContext>>& aToplevels) { + aToplevels.AppendElements(mToplevels); + } + + uint64_t Id() { return mId; } + + nsISupports* GetParentObject() const; + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Get or create a BrowsingContextGroup with the given ID. + static already_AddRefed<BrowsingContextGroup> GetOrCreate(uint64_t aId); + static already_AddRefed<BrowsingContextGroup> GetExisting(uint64_t aId); + static already_AddRefed<BrowsingContextGroup> Create( + bool aPotentiallyCrossOriginIsolated = false); + static already_AddRefed<BrowsingContextGroup> Select( + WindowContext* aParent, BrowsingContext* aOpener); + + // Like `Create` but only generates and reserves a new ID without actually + // creating the BrowsingContextGroup object. + static uint64_t CreateId(bool aPotentiallyCrossOriginIsolated = false); + + // For each 'ContentParent', except for 'aExcludedParent', + // associated with this group call 'aCallback'. + template <typename Func> + void EachOtherParent(ContentParent* aExcludedParent, Func&& aCallback) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + for (const auto& key : mSubscribers) { + if (key != aExcludedParent) { + aCallback(key); + } + } + } + + // For each 'ContentParent' associated with + // this group call 'aCallback'. + template <typename Func> + void EachParent(Func&& aCallback) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + for (const auto& key : mSubscribers) { + aCallback(key); + } + } + + nsresult QueuePostMessageEvent(already_AddRefed<nsIRunnable>&& aRunnable); + + void FlushPostMessageEvents(); + + // Increase or decrease the suspension level in InputTaskManager + void UpdateInputTaskManagerIfNeeded(bool aIsActive); + + static BrowsingContextGroup* GetChromeGroup(); + + void GetDocGroups(nsTArray<DocGroup*>& aDocGroups); + + // Called by Document when a Document needs to be added to a DocGroup. + already_AddRefed<DocGroup> AddDocument(const nsACString& aKey, + Document* aDocument); + + // Called by Document when a Document needs to be removed from a DocGroup. + // aDocGroup should be from aDocument. This is done to avoid the assert + // in GetDocGroup() which can crash when called during unlinking. + void RemoveDocument(Document* aDocument, DocGroup* aDocGroup); + + mozilla::ThrottledEventQueue* GetTimerEventQueue() const { + return mTimerEventQueue; + } + + mozilla::ThrottledEventQueue* GetWorkerEventQueue() const { + return mWorkerEventQueue; + } + + void SetAreDialogsEnabled(bool aAreDialogsEnabled) { + mAreDialogsEnabled = aAreDialogsEnabled; + } + + bool GetAreDialogsEnabled() { return mAreDialogsEnabled; } + + bool GetDialogAbuseCount() { return mDialogAbuseCount; } + + // For tests only. + void ResetDialogAbuseState(); + + bool DialogsAreBeingAbused(); + + TimeStamp GetLastDialogQuitTime() { return mLastDialogQuitTime; } + + void SetLastDialogQuitTime(TimeStamp aLastDialogQuitTime) { + mLastDialogQuitTime = aLastDialogQuitTime; + } + + // Whether all toplevel documents loaded in this group are allowed to be + // Cross-Origin Isolated. + // + // This does not reflect the actual value of `crossOriginIsolated`, as that + // also requires that the document is loaded within a `webCOOP+COEP` content + // process. + bool IsPotentiallyCrossOriginIsolated(); + + static void GetAllGroups(nsTArray<RefPtr<BrowsingContextGroup>>& aGroups); + + void IncInputEventSuspensionLevel(); + void DecInputEventSuspensionLevel(); + + void ChildDestroy(); + + private: + friend class CanonicalBrowsingContext; + + explicit BrowsingContextGroup(uint64_t aId); + ~BrowsingContextGroup(); + + void MaybeDestroy(); + void Destroy(); + + bool ShouldSuspendAllTopLevelContexts() const; + + bool HasActiveBC(); + void DecInputTaskManagerSuspensionLevel(); + void IncInputTaskManagerSuspensionLevel(); + + uint64_t mId; + + uint32_t mKeepAliveCount = 0; + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + bool mDestroyed = false; +#endif + + // A BrowsingContextGroup contains a series of {Browsing,Window}Context + // objects. They are addressed using a hashtable to avoid linear lookup when + // adding or removing elements from the set. + // + // FIXME: This list is only required over a counter to keep nested + // non-discarded contexts within discarded contexts alive. It should be + // removed in the future. + // FIXME: Consider introducing a better common base than `nsISupports`? + nsTHashSet<nsRefPtrHashKey<nsISupports>> mContexts; + + // The set of toplevel browsing contexts in the current BrowsingContextGroup. + nsTArray<RefPtr<BrowsingContext>> mToplevels; + + // Whether or not all toplevels in this group should be suspended + bool mToplevelsSuspended = false; + + // DocGroups are thread-safe, and not able to be cycle collected, + // but we still keep strong pointers. When all Documents are removed + // from DocGroup (by the BrowsingContextGroup), the DocGroup is + // removed from here. + nsRefPtrHashtable<nsCStringHashKey, DocGroup> mDocGroups; + + // The content process which will host documents in this BrowsingContextGroup + // which need to be loaded with a given remote type. + // + // A non-launching host process must also be a subscriber, though a launching + // host process may not yet be subscribed, and a subscriber need not be a host + // process. + nsRefPtrHashtable<nsCStringHashKey, ContentParent> mHosts; + + nsTHashSet<nsRefPtrHashKey<ContentParent>> mSubscribers; + + // A queue to store postMessage events during page load, the queue will be + // flushed once the page is loaded + RefPtr<mozilla::ThrottledEventQueue> mPostMessageEventQueue; + + RefPtr<mozilla::ThrottledEventQueue> mTimerEventQueue; + RefPtr<mozilla::ThrottledEventQueue> mWorkerEventQueue; + + // A counter to keep track of the input event suspension level of this BCG + // + // We use BrowsingContextGroup to emulate process isolation in Fission, so + // documents within the same the same BCG will behave like they share + // the same input task queue. + uint32_t mInputEventSuspensionLevel = 0; + // Whether this BCG has increased the suspension level in InputTaskManager + bool mHasIncreasedInputTaskManagerSuspensionLevel = false; + + // This flag keeps track of whether dialogs are + // currently enabled for windows of this group. + // It's OK to have these local to each process only because even if + // frames from two/three different sites (and thus, processes) coordinate a + // dialog abuse attack, they would only the double/triple number of dialogs, + // as it is still limited per-site. + bool mAreDialogsEnabled = true; + + // This counts the number of windows that have been opened in rapid succession + // (i.e. within dom.successive_dialog_time_limit of each other). It is reset + // to 0 once a dialog is opened after dom.successive_dialog_time_limit seconds + // have elapsed without any other dialogs. + // See comment for mAreDialogsEnabled as to why it's ok to have this local to + // each process. + uint32_t mDialogAbuseCount = 0; + + // This holds the time when the last modal dialog was shown. If more than + // MAX_DIALOG_LIMIT dialogs are shown within the time span defined by + // dom.successive_dialog_time_limit, we show a checkbox or confirmation prompt + // to allow disabling of further dialogs from windows in this BC group. + TimeStamp mLastDialogQuitTime; +}; +} // namespace dom +} // namespace mozilla + +inline void ImplCycleCollectionUnlink( + mozilla::dom::BrowsingContextGroup::KeepAlivePtr& aField) { + aField = nullptr; +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + mozilla::dom::BrowsingContextGroup::KeepAlivePtr& aField, const char* aName, + uint32_t aFlags = 0) { + CycleCollectionNoteChild(aCallback, aField.get(), aName, aFlags); +} + +#endif // !defined(mozilla_dom_BrowsingContextGroup_h) diff --git a/docshell/base/BrowsingContextWebProgress.cpp b/docshell/base/BrowsingContextWebProgress.cpp new file mode 100644 index 0000000000..2c6baf4fa8 --- /dev/null +++ b/docshell/base/BrowsingContextWebProgress.cpp @@ -0,0 +1,432 @@ +/* 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 "BrowsingContextWebProgress.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Logging.h" +#include "nsCOMPtr.h" +#include "nsIWebProgressListener.h" +#include "nsString.h" +#include "nsPrintfCString.h" +#include "nsIChannel.h" +#include "xptinfo.h" + +namespace mozilla { +namespace dom { + +static mozilla::LazyLogModule gBCWebProgressLog("BCWebProgress"); + +static nsCString DescribeBrowsingContext(CanonicalBrowsingContext* aContext); +static nsCString DescribeWebProgress(nsIWebProgress* aWebProgress); +static nsCString DescribeRequest(nsIRequest* aRequest); +static nsCString DescribeWebProgressFlags(uint32_t aFlags, + const nsACString& aPrefix); +static nsCString DescribeError(nsresult aError); + +NS_IMPL_CYCLE_COLLECTION(BrowsingContextWebProgress, mCurrentBrowsingContext) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(BrowsingContextWebProgress) +NS_IMPL_CYCLE_COLLECTING_RELEASE(BrowsingContextWebProgress) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BrowsingContextWebProgress) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebProgress) + NS_INTERFACE_MAP_ENTRY(nsIWebProgress) + NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) +NS_INTERFACE_MAP_END + +BrowsingContextWebProgress::BrowsingContextWebProgress( + CanonicalBrowsingContext* aBrowsingContext) + : mCurrentBrowsingContext(aBrowsingContext) {} + +BrowsingContextWebProgress::~BrowsingContextWebProgress() = default; + +NS_IMETHODIMP BrowsingContextWebProgress::AddProgressListener( + nsIWebProgressListener* aListener, uint32_t aNotifyMask) { + nsWeakPtr listener = do_GetWeakReference(aListener); + if (!listener) { + return NS_ERROR_INVALID_ARG; + } + + if (mListenerInfoList.Contains(listener)) { + // The listener is already registered! + return NS_ERROR_FAILURE; + } + + mListenerInfoList.AppendElement(ListenerInfo(listener, aNotifyMask)); + return NS_OK; +} + +NS_IMETHODIMP BrowsingContextWebProgress::RemoveProgressListener( + nsIWebProgressListener* aListener) { + nsWeakPtr listener = do_GetWeakReference(aListener); + if (!listener) { + return NS_ERROR_INVALID_ARG; + } + + return mListenerInfoList.RemoveElement(listener) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP BrowsingContextWebProgress::GetBrowsingContextXPCOM( + BrowsingContext** aBrowsingContext) { + NS_IF_ADDREF(*aBrowsingContext = mCurrentBrowsingContext); + return NS_OK; +} + +BrowsingContext* BrowsingContextWebProgress::GetBrowsingContext() { + return mCurrentBrowsingContext; +} + +NS_IMETHODIMP BrowsingContextWebProgress::GetDOMWindow( + mozIDOMWindowProxy** aDOMWindow) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP BrowsingContextWebProgress::GetIsTopLevel(bool* aIsTopLevel) { + *aIsTopLevel = mCurrentBrowsingContext->IsTop(); + return NS_OK; +} + +NS_IMETHODIMP BrowsingContextWebProgress::GetIsLoadingDocument( + bool* aIsLoadingDocument) { + *aIsLoadingDocument = mIsLoadingDocument; + return NS_OK; +} + +NS_IMETHODIMP BrowsingContextWebProgress::GetLoadType(uint32_t* aLoadType) { + *aLoadType = mLoadType; + return NS_OK; +} + +NS_IMETHODIMP BrowsingContextWebProgress::GetTarget(nsIEventTarget** aTarget) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP BrowsingContextWebProgress::SetTarget(nsIEventTarget* aTarget) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void BrowsingContextWebProgress::UpdateAndNotifyListeners( + uint32_t aFlag, + const std::function<void(nsIWebProgressListener*)>& aCallback) { + RefPtr<BrowsingContextWebProgress> kungFuDeathGrip = this; + + ListenerArray::ForwardIterator iter(mListenerInfoList); + while (iter.HasMore()) { + ListenerInfo& info = iter.GetNext(); + if (!(info.mNotifyMask & aFlag)) { + continue; + } + + nsCOMPtr<nsIWebProgressListener> listener = + do_QueryReferent(info.mWeakListener); + if (!listener) { + mListenerInfoList.RemoveElement(info); + continue; + } + + aCallback(listener); + } + + mListenerInfoList.Compact(); + + // Notify the parent BrowsingContextWebProgress of the event to continue + // propagating. + auto* parent = mCurrentBrowsingContext->GetParent(); + if (parent && parent->GetWebProgress()) { + aCallback(parent->GetWebProgress()); + } +} + +void BrowsingContextWebProgress::ContextDiscarded() { + if (!mIsLoadingDocument) { + return; + } + + // If our BrowsingContext is being discarded while still loading a document, + // fire a synthetic `STATE_STOP` to end the ongoing load. + MOZ_LOG(gBCWebProgressLog, LogLevel::Info, + ("Discarded while loading %s", + DescribeBrowsingContext(mCurrentBrowsingContext).get())); + + // This matches what nsDocLoader::doStopDocumentLoad does, except we don't + // bother notifying for `STATE_STOP | STATE_IS_DOCUMENT`, + // nsBrowserStatusFilter would filter it out before it gets to the parent + // process. + nsCOMPtr<nsIRequest> request = mLoadingDocumentRequest; + OnStateChange(this, request, STATE_STOP | STATE_IS_WINDOW | STATE_IS_NETWORK, + NS_ERROR_ABORT); +} + +void BrowsingContextWebProgress::ContextReplaced( + CanonicalBrowsingContext* aNewContext) { + mCurrentBrowsingContext = aNewContext; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIWebProgressListener + +NS_IMETHODIMP +BrowsingContextWebProgress::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aStateFlags, + nsresult aStatus) { + MOZ_LOG( + gBCWebProgressLog, LogLevel::Info, + ("OnStateChange(%s, %s, %s, %s) on %s", + DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(), + DescribeWebProgressFlags(aStateFlags, "STATE_"_ns).get(), + DescribeError(aStatus).get(), + DescribeBrowsingContext(mCurrentBrowsingContext).get())); + + bool targetIsThis = aWebProgress == this; + + // We may receive a request from an in-process nsDocShell which doesn't have + // `aWebProgress == this` which we should still consider as targeting + // ourselves. + if (nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(aWebProgress); + docShell && docShell->GetBrowsingContext() == mCurrentBrowsingContext) { + targetIsThis = true; + aWebProgress->GetLoadType(&mLoadType); + } + + // Track `mIsLoadingDocument` based on the notifications we've received so far + // if the nsIWebProgress being targeted is this one. + if (targetIsThis) { + constexpr uint32_t startFlags = nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_DOCUMENT | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_WINDOW | + nsIWebProgressListener::STATE_IS_NETWORK; + constexpr uint32_t stopFlags = nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_WINDOW; + constexpr uint32_t redirectFlags = + nsIWebProgressListener::STATE_IS_REDIRECTED_DOCUMENT; + if ((aStateFlags & startFlags) == startFlags) { + if (mIsLoadingDocument) { + // We received a duplicate `STATE_START` notification, silence this + // notification until we receive the matching `STATE_STOP` to not fire + // duplicate `STATE_START` notifications into frontend on process + // switches. + return NS_OK; + } + mIsLoadingDocument = true; + + // Record the request we started the load with, so we can emit a synthetic + // `STATE_STOP` notification if the BrowsingContext is discarded before + // the notification arrives. + mLoadingDocumentRequest = aRequest; + } else if ((aStateFlags & stopFlags) == stopFlags) { + // We've received a `STATE_STOP` notification targeting this web progress, + // clear our loading document flag. + mIsLoadingDocument = false; + mLoadingDocumentRequest = nullptr; + } else if (mIsLoadingDocument && + (aStateFlags & redirectFlags) == redirectFlags) { + // If we see a redirected document load, update the loading request which + // we'll emit the synthetic STATE_STOP notification with. + mLoadingDocumentRequest = aRequest; + } + } + + UpdateAndNotifyListeners( + ((aStateFlags >> 16) & nsIWebProgress::NOTIFY_STATE_ALL), + [&](nsIWebProgressListener* listener) { + listener->OnStateChange(aWebProgress, aRequest, aStateFlags, aStatus); + }); + return NS_OK; +} + +NS_IMETHODIMP +BrowsingContextWebProgress::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + MOZ_LOG( + gBCWebProgressLog, LogLevel::Info, + ("OnProgressChange(%s, %s, %d, %d, %d, %d) on %s", + DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(), + aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, + DescribeBrowsingContext(mCurrentBrowsingContext).get())); + UpdateAndNotifyListeners( + nsIWebProgress::NOTIFY_PROGRESS, [&](nsIWebProgressListener* listener) { + listener->OnProgressChange(aWebProgress, aRequest, aCurSelfProgress, + aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress); + }); + return NS_OK; +} + +NS_IMETHODIMP +BrowsingContextWebProgress::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI* aLocation, + uint32_t aFlags) { + MOZ_LOG( + gBCWebProgressLog, LogLevel::Info, + ("OnLocationChange(%s, %s, %s, %s) on %s", + DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(), + aLocation ? aLocation->GetSpecOrDefault().get() : "<null>", + DescribeWebProgressFlags(aFlags, "LOCATION_CHANGE_"_ns).get(), + DescribeBrowsingContext(mCurrentBrowsingContext).get())); + UpdateAndNotifyListeners( + nsIWebProgress::NOTIFY_LOCATION, [&](nsIWebProgressListener* listener) { + listener->OnLocationChange(aWebProgress, aRequest, aLocation, aFlags); + }); + return NS_OK; +} + +NS_IMETHODIMP +BrowsingContextWebProgress::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) { + MOZ_LOG( + gBCWebProgressLog, LogLevel::Info, + ("OnStatusChange(%s, %s, %s, \"%s\") on %s", + DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(), + DescribeError(aStatus).get(), NS_ConvertUTF16toUTF8(aMessage).get(), + DescribeBrowsingContext(mCurrentBrowsingContext).get())); + UpdateAndNotifyListeners( + nsIWebProgress::NOTIFY_STATUS, [&](nsIWebProgressListener* listener) { + listener->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage); + }); + return NS_OK; +} + +NS_IMETHODIMP +BrowsingContextWebProgress::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aState) { + MOZ_LOG( + gBCWebProgressLog, LogLevel::Info, + ("OnSecurityChange(%s, %s, %x) on %s", + DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(), + aState, DescribeBrowsingContext(mCurrentBrowsingContext).get())); + UpdateAndNotifyListeners( + nsIWebProgress::NOTIFY_SECURITY, [&](nsIWebProgressListener* listener) { + listener->OnSecurityChange(aWebProgress, aRequest, aState); + }); + return NS_OK; +} + +NS_IMETHODIMP +BrowsingContextWebProgress::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + MOZ_LOG( + gBCWebProgressLog, LogLevel::Info, + ("OnContentBlockingEvent(%s, %s, %x) on %s", + DescribeWebProgress(aWebProgress).get(), DescribeRequest(aRequest).get(), + aEvent, DescribeBrowsingContext(mCurrentBrowsingContext).get())); + UpdateAndNotifyListeners(nsIWebProgress::NOTIFY_CONTENT_BLOCKING, + [&](nsIWebProgressListener* listener) { + listener->OnContentBlockingEvent(aWebProgress, + aRequest, aEvent); + }); + return NS_OK; +} + +NS_IMETHODIMP +BrowsingContextWebProgress::GetDocumentRequest(nsIRequest** aRequest) { + NS_IF_ADDREF(*aRequest = mLoadingDocumentRequest); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// Helper methods for notification logging + +static nsCString DescribeBrowsingContext(CanonicalBrowsingContext* aContext) { + if (!aContext) { + return "<null>"_ns; + } + + nsCOMPtr<nsIURI> currentURI = aContext->GetCurrentURI(); + return nsPrintfCString( + "{top:%d, id:%" PRIx64 ", url:%s}", aContext->IsTop(), aContext->Id(), + currentURI ? currentURI->GetSpecOrDefault().get() : "<null>"); +} + +static nsCString DescribeWebProgress(nsIWebProgress* aWebProgress) { + if (!aWebProgress) { + return "<null>"_ns; + } + + bool isTopLevel = false; + aWebProgress->GetIsTopLevel(&isTopLevel); + bool isLoadingDocument = false; + aWebProgress->GetIsLoadingDocument(&isLoadingDocument); + return nsPrintfCString("{isTopLevel:%d, isLoadingDocument:%d}", isTopLevel, + isLoadingDocument); +} + +static nsCString DescribeRequest(nsIRequest* aRequest) { + if (!aRequest) { + return "<null>"_ns; + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (!channel) { + return "<non-channel>"_ns; + } + + nsCOMPtr<nsIURI> originalURI; + channel->GetOriginalURI(getter_AddRefs(originalURI)); + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + + return nsPrintfCString( + "{URI:%s, originalURI:%s}", + uri ? uri->GetSpecOrDefault().get() : "<null>", + originalURI ? originalURI->GetSpecOrDefault().get() : "<null>"); +} + +static nsCString DescribeWebProgressFlags(uint32_t aFlags, + const nsACString& aPrefix) { + nsCString flags; + uint32_t remaining = aFlags; + + // Hackily fetch the names of each constant from the XPT information used for + // reflecting it into JS. This doesn't need to be reliable and just exists as + // a logging aid. + // + // If a change to xpt in the future breaks this code, just delete it and + // replace it with a normal hex log. + if (const auto* ifaceInfo = + nsXPTInterfaceInfo::ByIID(NS_GET_IID(nsIWebProgressListener))) { + for (uint16_t i = 0; i < ifaceInfo->ConstantCount(); ++i) { + const auto& constInfo = ifaceInfo->Constant(i); + nsDependentCString name(constInfo.Name()); + if (!StringBeginsWith(name, aPrefix)) { + continue; + } + + if (remaining & constInfo.mValue) { + remaining &= ~constInfo.mValue; + if (!flags.IsEmpty()) { + flags.AppendLiteral("|"); + } + flags.Append(name); + } + } + } + if (remaining != 0 || flags.IsEmpty()) { + if (!flags.IsEmpty()) { + flags.AppendLiteral("|"); + } + flags.AppendInt(remaining, 16); + } + return flags; +} + +static nsCString DescribeError(nsresult aError) { + nsCString name; + GetErrorName(aError, name); + return name; +} + +} // namespace dom +} // namespace mozilla diff --git a/docshell/base/BrowsingContextWebProgress.h b/docshell/base/BrowsingContextWebProgress.h new file mode 100644 index 0000000000..b39fb2545d --- /dev/null +++ b/docshell/base/BrowsingContextWebProgress.h @@ -0,0 +1,98 @@ +/* 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/. */ + +#ifndef mozilla_dom_BrowsingContextWebProgress_h +#define mozilla_dom_BrowsingContextWebProgress_h + +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsTObserverArray.h" +#include "nsWeakReference.h" +#include "nsCycleCollectionParticipant.h" + +namespace mozilla::dom { + +class CanonicalBrowsingContext; + +/// Object acting as the nsIWebProgress instance for a BrowsingContext over its +/// lifetime. +/// +/// An active toplevel CanonicalBrowsingContext will always have a +/// BrowsingContextWebProgress, which will be moved between contexts as +/// BrowsingContextGroup-changing loads are performed. +/// +/// Subframes will only have a `BrowsingContextWebProgress` if they are loaded +/// in a content process, and will use the nsDocShell instead if they are loaded +/// in the parent process, as parent process documents cannot have or be +/// out-of-process iframes. +class BrowsingContextWebProgress final : public nsIWebProgress, + public nsIWebProgressListener { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(BrowsingContextWebProgress, + nsIWebProgress) + NS_DECL_NSIWEBPROGRESS + NS_DECL_NSIWEBPROGRESSLISTENER + + explicit BrowsingContextWebProgress( + CanonicalBrowsingContext* aBrowsingContext); + + struct ListenerInfo { + ListenerInfo(nsIWeakReference* aListener, unsigned long aNotifyMask) + : mWeakListener(aListener), mNotifyMask(aNotifyMask) {} + + bool operator==(const ListenerInfo& aOther) const { + return mWeakListener == aOther.mWeakListener; + } + bool operator==(const nsWeakPtr& aOther) const { + return mWeakListener == aOther; + } + + // Weak pointer for the nsIWebProgressListener... + nsWeakPtr mWeakListener; + + // Mask indicating which notifications the listener wants to receive. + unsigned long mNotifyMask; + }; + + void ContextDiscarded(); + void ContextReplaced(CanonicalBrowsingContext* aNewContext); + + void SetLoadType(uint32_t aLoadType) { mLoadType = aLoadType; } + + private: + virtual ~BrowsingContextWebProgress(); + + void UpdateAndNotifyListeners( + uint32_t aFlag, + const std::function<void(nsIWebProgressListener*)>& aCallback); + + using ListenerArray = nsAutoTObserverArray<ListenerInfo, 4>; + ListenerArray mListenerInfoList; + + // The current BrowsingContext which owns this BrowsingContextWebProgress. + // This context may change during navigations and may not be fully attached at + // all times. + RefPtr<CanonicalBrowsingContext> mCurrentBrowsingContext; + + // The current request being actively loaded by the BrowsingContext. Only set + // while mIsLoadingDocument is true, and is used to fire STATE_STOP + // notifications if the BrowsingContext is discarded while the load is + // ongoing. + nsCOMPtr<nsIRequest> mLoadingDocumentRequest; + + // The most recent load type observed for this BrowsingContextWebProgress. + uint32_t mLoadType = 0; + + // Are we currently in the process of loading a document? This is true between + // the `STATE_START` notification from content and the `STATE_STOP` + // notification being received. Duplicate `STATE_START` events may be + // discarded while loading a document to avoid noise caused by process + // switches. + bool mIsLoadingDocument = false; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_BrowsingContextWebProgress_h diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp new file mode 100644 index 0000000000..b6bedf7ba9 --- /dev/null +++ b/docshell/base/CanonicalBrowsingContext.cpp @@ -0,0 +1,3069 @@ +/* -*- 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/. */ + +#include "mozilla/dom/CanonicalBrowsingContext.h" + +#include "ErrorList.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventForwards.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/BrowsingContextBinding.h" +#include "mozilla/dom/BrowsingContextGroup.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/PBrowserParent.h" +#include "mozilla/dom/PBackgroundSessionStorageCache.h" +#include "mozilla/dom/PWindowGlobalParent.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/ContentProcessManager.h" +#include "mozilla/dom/MediaController.h" +#include "mozilla/dom/MediaControlService.h" +#include "mozilla/dom/ContentPlaybackController.h" +#include "mozilla/dom/SessionStorageManager.h" +#include "mozilla/ipc/ProtocolUtils.h" +#ifdef NS_PRINTING +# include "mozilla/layout/RemotePrintJobParent.h" +#endif +#include "mozilla/net/DocumentLoadListener.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_docshell.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/Telemetry.h" +#include "nsILayoutHistoryState.h" +#include "nsIPrintSettings.h" +#include "nsIPrintSettingsService.h" +#include "nsISupports.h" +#include "nsIWebNavigation.h" +#include "mozilla/MozPromiseInlines.h" +#include "nsDocShell.h" +#include "nsFrameLoader.h" +#include "nsFrameLoaderOwner.h" +#include "nsGlobalWindowOuter.h" +#include "nsIWebBrowserChrome.h" +#include "nsIXULRuntime.h" +#include "nsNetUtil.h" +#include "nsSHistory.h" +#include "nsSecureBrowserUI.h" +#include "nsQueryObject.h" +#include "nsBrowserStatusFilter.h" +#include "nsIBrowser.h" +#include "nsTHashSet.h" +#include "SessionStoreFunctions.h" +#include "nsIXPConnect.h" +#include "nsImportModule.h" +#include "UnitTransforms.h" + +using namespace mozilla::ipc; + +extern mozilla::LazyLogModule gAutoplayPermissionLog; +extern mozilla::LazyLogModule gSHLog; +extern mozilla::LazyLogModule gSHIPBFCacheLog; + +#define AUTOPLAY_LOG(msg, ...) \ + MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +static mozilla::LazyLogModule sPBContext("PBContext"); + +// Global count of canonical browsing contexts with the private attribute set +static uint32_t gNumberOfPrivateContexts = 0; + +// Current parent process epoch for parent initiated navigations +static uint64_t gParentInitiatedNavigationEpoch = 0; + +static void IncreasePrivateCount() { + gNumberOfPrivateContexts++; + MOZ_LOG(sPBContext, mozilla::LogLevel::Debug, + ("%s: Private browsing context count %d -> %d", __func__, + gNumberOfPrivateContexts - 1, gNumberOfPrivateContexts)); + if (gNumberOfPrivateContexts > 1) { + return; + } + + static bool sHasSeenPrivateContext = false; + if (!sHasSeenPrivateContext) { + sHasSeenPrivateContext = true; + mozilla::Telemetry::ScalarSet( + mozilla::Telemetry::ScalarID::DOM_PARENTPROCESS_PRIVATE_WINDOW_USED, + true); + } +} + +static void DecreasePrivateCount() { + MOZ_ASSERT(gNumberOfPrivateContexts > 0); + gNumberOfPrivateContexts--; + + MOZ_LOG(sPBContext, mozilla::LogLevel::Debug, + ("%s: Private browsing context count %d -> %d", __func__, + gNumberOfPrivateContexts + 1, gNumberOfPrivateContexts)); + if (!gNumberOfPrivateContexts && + !mozilla::StaticPrefs::browser_privatebrowsing_autostart()) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + MOZ_LOG(sPBContext, mozilla::LogLevel::Debug, + ("%s: last-pb-context-exited fired", __func__)); + observerService->NotifyObservers(nullptr, "last-pb-context-exited", + nullptr); + } + } +} + +namespace mozilla { +namespace dom { + +extern mozilla::LazyLogModule gUserInteractionPRLog; + +#define USER_ACTIVATION_LOG(msg, ...) \ + MOZ_LOG(gUserInteractionPRLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +CanonicalBrowsingContext::CanonicalBrowsingContext(WindowContext* aParentWindow, + BrowsingContextGroup* aGroup, + uint64_t aBrowsingContextId, + uint64_t aOwnerProcessId, + uint64_t aEmbedderProcessId, + BrowsingContext::Type aType, + FieldValues&& aInit) + : BrowsingContext(aParentWindow, aGroup, aBrowsingContextId, aType, + std::move(aInit)), + mProcessId(aOwnerProcessId), + mEmbedderProcessId(aEmbedderProcessId), + mPermanentKey(JS::NullValue()) { + // You are only ever allowed to create CanonicalBrowsingContexts in the + // parent process. + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + + // The initial URI in a BrowsingContext is always "about:blank". + MOZ_ALWAYS_SUCCEEDS( + NS_NewURI(getter_AddRefs(mCurrentRemoteURI), "about:blank")); + + mozilla::HoldJSObjects(this); +} + +CanonicalBrowsingContext::~CanonicalBrowsingContext() { + mPermanentKey.setNull(); + + mozilla::DropJSObjects(this); + + if (mSessionHistory) { + mSessionHistory->SetBrowsingContext(nullptr); + } +} + +/* static */ +already_AddRefed<CanonicalBrowsingContext> CanonicalBrowsingContext::Get( + uint64_t aId) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + return BrowsingContext::Get(aId).downcast<CanonicalBrowsingContext>(); +} + +/* static */ +CanonicalBrowsingContext* CanonicalBrowsingContext::Cast( + BrowsingContext* aContext) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + return static_cast<CanonicalBrowsingContext*>(aContext); +} + +/* static */ +const CanonicalBrowsingContext* CanonicalBrowsingContext::Cast( + const BrowsingContext* aContext) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + return static_cast<const CanonicalBrowsingContext*>(aContext); +} + +already_AddRefed<CanonicalBrowsingContext> CanonicalBrowsingContext::Cast( + already_AddRefed<BrowsingContext>&& aContext) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + return aContext.downcast<CanonicalBrowsingContext>(); +} + +ContentParent* CanonicalBrowsingContext::GetContentParent() const { + if (mProcessId == 0) { + return nullptr; + } + + ContentProcessManager* cpm = ContentProcessManager::GetSingleton(); + if (!cpm) { + return nullptr; + } + return cpm->GetContentProcessById(ContentParentId(mProcessId)); +} + +void CanonicalBrowsingContext::GetCurrentRemoteType(nsACString& aRemoteType, + ErrorResult& aRv) const { + // If we're in the parent process, dump out the void string. + if (mProcessId == 0) { + aRemoteType = NOT_REMOTE_TYPE; + return; + } + + ContentParent* cp = GetContentParent(); + if (!cp) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + aRemoteType = cp->GetRemoteType(); +} + +void CanonicalBrowsingContext::SetOwnerProcessId(uint64_t aProcessId) { + MOZ_LOG(GetLog(), LogLevel::Debug, + ("SetOwnerProcessId for 0x%08" PRIx64 " (0x%08" PRIx64 + " -> 0x%08" PRIx64 ")", + Id(), mProcessId, aProcessId)); + + mProcessId = aProcessId; +} + +nsISecureBrowserUI* CanonicalBrowsingContext::GetSecureBrowserUI() { + if (!IsTop()) { + return nullptr; + } + if (!mSecureBrowserUI) { + mSecureBrowserUI = new nsSecureBrowserUI(this); + } + return mSecureBrowserUI; +} + +namespace { +// The DocShellProgressBridge is attached to a root content docshell loaded in +// the parent process. Notifications are paired up with the docshell which they +// came from, so that they can be fired to the correct +// BrowsingContextWebProgress and bubble through this tree separately. +// +// Notifications are filtered by a nsBrowserStatusFilter before being received +// by the DocShellProgressBridge. +class DocShellProgressBridge : public nsIWebProgressListener { + public: + NS_DECL_ISUPPORTS + // NOTE: This relies in the expansion of `NS_FORWARD_SAFE` and all listener + // methods accepting an `aWebProgress` argument. If this changes in the + // future, this may need to be written manually. + NS_FORWARD_SAFE_NSIWEBPROGRESSLISTENER(GetTargetContext(aWebProgress)) + + explicit DocShellProgressBridge(uint64_t aTopContextId) + : mTopContextId(aTopContextId) {} + + private: + virtual ~DocShellProgressBridge() = default; + + nsIWebProgressListener* GetTargetContext(nsIWebProgress* aWebProgress) { + RefPtr<CanonicalBrowsingContext> context; + if (nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(aWebProgress)) { + context = docShell->GetBrowsingContext()->Canonical(); + } else { + context = CanonicalBrowsingContext::Get(mTopContextId); + } + return context && !context->IsDiscarded() ? context->GetWebProgress() + : nullptr; + } + + uint64_t mTopContextId = 0; +}; + +NS_IMPL_ISUPPORTS(DocShellProgressBridge, nsIWebProgressListener) +} // namespace + +void CanonicalBrowsingContext::MaybeAddAsProgressListener( + nsIWebProgress* aWebProgress) { + // Only add as a listener if the created docshell is a toplevel content + // docshell. We'll get notifications for all of our subframes through a single + // listener. + if (!IsTopContent()) { + return; + } + + if (!mDocShellProgressBridge) { + mDocShellProgressBridge = new DocShellProgressBridge(Id()); + mStatusFilter = new nsBrowserStatusFilter(); + mStatusFilter->AddProgressListener(mDocShellProgressBridge, + nsIWebProgress::NOTIFY_ALL); + } + + aWebProgress->AddProgressListener(mStatusFilter, nsIWebProgress::NOTIFY_ALL); +} + +void CanonicalBrowsingContext::ReplacedBy( + CanonicalBrowsingContext* aNewContext, + const NavigationIsolationOptions& aRemotenessOptions) { + MOZ_ASSERT(!aNewContext->mWebProgress); + MOZ_ASSERT(!aNewContext->mSessionHistory); + MOZ_ASSERT(IsTop() && aNewContext->IsTop()); + + mIsReplaced = true; + aNewContext->mIsReplaced = false; + + if (mStatusFilter) { + mStatusFilter->RemoveProgressListener(mDocShellProgressBridge); + mStatusFilter = nullptr; + } + + mWebProgress->ContextReplaced(aNewContext); + aNewContext->mWebProgress = std::move(mWebProgress); + + // Use the Transaction for the fields which need to be updated whether or not + // the new context has been attached before. + // SetWithoutSyncing can be used if context hasn't been attached. + Transaction txn; + txn.SetBrowserId(GetBrowserId()); + txn.SetIsAppTab(GetIsAppTab()); + txn.SetHasSiblings(GetHasSiblings()); + txn.SetHistoryID(GetHistoryID()); + txn.SetExplicitActive(GetExplicitActive()); + txn.SetEmbedderColorSchemes(GetEmbedderColorSchemes()); + txn.SetHasRestoreData(GetHasRestoreData()); + txn.SetShouldDelayMediaFromStart(GetShouldDelayMediaFromStart()); + + // Propagate some settings on BrowsingContext replacement so they're not lost + // on bfcached navigations. These are important for GeckoView (see bug + // 1781936). + txn.SetAllowJavascript(GetAllowJavascript()); + txn.SetForceEnableTrackingProtection(GetForceEnableTrackingProtection()); + txn.SetUserAgentOverride(GetUserAgentOverride()); + txn.SetSuspendMediaWhenInactive(GetSuspendMediaWhenInactive()); + txn.SetDisplayMode(GetDisplayMode()); + txn.SetForceDesktopViewport(GetForceDesktopViewport()); + txn.SetIsUnderHiddenEmbedderElement(GetIsUnderHiddenEmbedderElement()); + + // Propagate the default load flags so that the TRR mode flags are forwarded + // to the new browsing context. See bug 1828643. + txn.SetDefaultLoadFlags(GetDefaultLoadFlags()); + + // As this is a different BrowsingContext, set InitialSandboxFlags to the + // current flags in the new context so that they also apply to any initial + // about:blank documents created in it. + txn.SetSandboxFlags(GetSandboxFlags()); + txn.SetInitialSandboxFlags(GetSandboxFlags()); + txn.SetTargetTopLevelLinkClicksToBlankInternal( + TargetTopLevelLinkClicksToBlank()); + if (aNewContext->EverAttached()) { + MOZ_ALWAYS_SUCCEEDS(txn.Commit(aNewContext)); + } else { + txn.CommitWithoutSyncing(aNewContext); + } + + aNewContext->mRestoreState = mRestoreState.forget(); + MOZ_ALWAYS_SUCCEEDS(SetHasRestoreData(false)); + + // XXXBFCache name handling is still a bit broken in Fission in general, + // at least in case name should be cleared. + if (aRemotenessOptions.mTryUseBFCache) { + MOZ_ASSERT(!aNewContext->EverAttached()); + aNewContext->mFields.SetWithoutSyncing<IDX_Name>(GetName()); + // We don't copy over HasLoadedNonInitialDocument here, we'll actually end + // up loading a new initial document at this point, before the real load. + // The real load will then end up setting HasLoadedNonInitialDocument to + // true. + } + + if (mSessionHistory) { + mSessionHistory->SetBrowsingContext(aNewContext); + // At this point we will be creating a new ChildSHistory in the child. + // That means that the child's epoch will be reset, so it makes sense to + // reset the epoch in the parent too. + mSessionHistory->SetEpoch(0, Nothing()); + mSessionHistory.swap(aNewContext->mSessionHistory); + RefPtr<ChildSHistory> childSHistory = ForgetChildSHistory(); + aNewContext->SetChildSHistory(childSHistory); + } + + if (mozilla::SessionHistoryInParent()) { + BackgroundSessionStorageManager::PropagateManager(Id(), aNewContext->Id()); + } + + // Transfer the ownership of the priority active status from the old context + // to the new context. + aNewContext->mPriorityActive = mPriorityActive; + mPriorityActive = false; + + MOZ_ASSERT(aNewContext->mLoadingEntries.IsEmpty()); + mLoadingEntries.SwapElements(aNewContext->mLoadingEntries); + MOZ_ASSERT(!aNewContext->mActiveEntry); + mActiveEntry.swap(aNewContext->mActiveEntry); + + aNewContext->mPermanentKey = mPermanentKey; + mPermanentKey.setNull(); +} + +void CanonicalBrowsingContext::UpdateSecurityState() { + if (mSecureBrowserUI) { + mSecureBrowserUI->RecomputeSecurityFlags(); + } +} + +void CanonicalBrowsingContext::GetWindowGlobals( + nsTArray<RefPtr<WindowGlobalParent>>& aWindows) { + aWindows.SetCapacity(GetWindowContexts().Length()); + for (auto& window : GetWindowContexts()) { + aWindows.AppendElement(static_cast<WindowGlobalParent*>(window.get())); + } +} + +WindowGlobalParent* CanonicalBrowsingContext::GetCurrentWindowGlobal() const { + return static_cast<WindowGlobalParent*>(GetCurrentWindowContext()); +} + +WindowGlobalParent* CanonicalBrowsingContext::GetParentWindowContext() { + return static_cast<WindowGlobalParent*>( + BrowsingContext::GetParentWindowContext()); +} + +WindowGlobalParent* CanonicalBrowsingContext::GetTopWindowContext() { + return static_cast<WindowGlobalParent*>( + BrowsingContext::GetTopWindowContext()); +} + +already_AddRefed<nsIWidget> +CanonicalBrowsingContext::GetParentProcessWidgetContaining() { + // If our document is loaded in-process, such as chrome documents, get the + // widget directly from our outer window. Otherwise, try to get the widget + // from the toplevel content's browser's element. + nsCOMPtr<nsIWidget> widget; + if (nsGlobalWindowOuter* window = nsGlobalWindowOuter::Cast(GetDOMWindow())) { + widget = window->GetNearestWidget(); + } else if (Element* topEmbedder = Top()->GetEmbedderElement()) { + widget = nsContentUtils::WidgetForContent(topEmbedder); + if (!widget) { + widget = nsContentUtils::WidgetForDocument(topEmbedder->OwnerDoc()); + } + } + + if (widget) { + widget = widget->GetTopLevelWidget(); + } + + return widget.forget(); +} + +already_AddRefed<nsIBrowserDOMWindow> +CanonicalBrowsingContext::GetBrowserDOMWindow() { + RefPtr<CanonicalBrowsingContext> chromeTop = TopCrossChromeBoundary(); + if (nsCOMPtr<nsIDOMChromeWindow> chromeWin = + do_QueryInterface(chromeTop->GetDOMWindow())) { + nsCOMPtr<nsIBrowserDOMWindow> bdw; + if (NS_SUCCEEDED(chromeWin->GetBrowserDOMWindow(getter_AddRefs(bdw)))) { + return bdw.forget(); + } + } + return nullptr; +} + +already_AddRefed<WindowGlobalParent> +CanonicalBrowsingContext::GetEmbedderWindowGlobal() const { + uint64_t windowId = GetEmbedderInnerWindowId(); + if (windowId == 0) { + return nullptr; + } + + return WindowGlobalParent::GetByInnerWindowId(windowId); +} + +already_AddRefed<CanonicalBrowsingContext> +CanonicalBrowsingContext::GetParentCrossChromeBoundary() { + if (GetParent()) { + return do_AddRef(Cast(GetParent())); + } + if (GetEmbedderElement()) { + return do_AddRef( + Cast(GetEmbedderElement()->OwnerDoc()->GetBrowsingContext())); + } + return nullptr; +} + +already_AddRefed<CanonicalBrowsingContext> +CanonicalBrowsingContext::TopCrossChromeBoundary() { + RefPtr<CanonicalBrowsingContext> bc(this); + while (RefPtr<CanonicalBrowsingContext> parent = + bc->GetParentCrossChromeBoundary()) { + bc = parent.forget(); + } + return bc.forget(); +} + +Nullable<WindowProxyHolder> CanonicalBrowsingContext::GetTopChromeWindow() { + RefPtr<CanonicalBrowsingContext> bc = TopCrossChromeBoundary(); + if (bc->IsChrome()) { + return WindowProxyHolder(bc.forget()); + } + return nullptr; +} + +nsISHistory* CanonicalBrowsingContext::GetSessionHistory() { + if (!IsTop()) { + return Cast(Top())->GetSessionHistory(); + } + + // Check GetChildSessionHistory() to make sure that this BrowsingContext has + // session history enabled. + if (!mSessionHistory && GetChildSessionHistory()) { + mSessionHistory = new nsSHistory(this); + } + + return mSessionHistory; +} + +SessionHistoryEntry* CanonicalBrowsingContext::GetActiveSessionHistoryEntry() { + return mActiveEntry; +} + +void CanonicalBrowsingContext::SetActiveSessionHistoryEntry( + SessionHistoryEntry* aEntry) { + mActiveEntry = aEntry; +} + +bool CanonicalBrowsingContext::HasHistoryEntry(nsISHEntry* aEntry) { + // XXX Should we check also loading entries? + return aEntry && mActiveEntry == aEntry; +} + +void CanonicalBrowsingContext::SwapHistoryEntries(nsISHEntry* aOldEntry, + nsISHEntry* aNewEntry) { + // XXX Should we check also loading entries? + if (mActiveEntry == aOldEntry) { + nsCOMPtr<SessionHistoryEntry> newEntry = do_QueryInterface(aNewEntry); + mActiveEntry = newEntry.forget(); + } +} + +void CanonicalBrowsingContext::AddLoadingSessionHistoryEntry( + uint64_t aLoadId, SessionHistoryEntry* aEntry) { + Unused << SetHistoryID(aEntry->DocshellID()); + mLoadingEntries.AppendElement(LoadingSessionHistoryEntry{aLoadId, aEntry}); +} + +void CanonicalBrowsingContext::GetLoadingSessionHistoryInfoFromParent( + Maybe<LoadingSessionHistoryInfo>& aLoadingInfo) { + nsISHistory* shistory = GetSessionHistory(); + if (!shistory || !GetParent()) { + return; + } + + SessionHistoryEntry* parentSHE = + GetParent()->Canonical()->GetActiveSessionHistoryEntry(); + if (parentSHE) { + int32_t index = -1; + for (BrowsingContext* sibling : GetParent()->Children()) { + ++index; + if (sibling == this) { + nsCOMPtr<nsISHEntry> shEntry; + parentSHE->GetChildSHEntryIfHasNoDynamicallyAddedChild( + index, getter_AddRefs(shEntry)); + nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(shEntry); + if (she) { + aLoadingInfo.emplace(she); + mLoadingEntries.AppendElement(LoadingSessionHistoryEntry{ + aLoadingInfo.value().mLoadId, she.get()}); + Unused << SetHistoryID(she->DocshellID()); + } + break; + } + } + } +} + +UniquePtr<LoadingSessionHistoryInfo> +CanonicalBrowsingContext::CreateLoadingSessionHistoryEntryForLoad( + nsDocShellLoadState* aLoadState, SessionHistoryEntry* existingEntry, + nsIChannel* aChannel) { + RefPtr<SessionHistoryEntry> entry; + const LoadingSessionHistoryInfo* existingLoadingInfo = + aLoadState->GetLoadingSessionHistoryInfo(); + MOZ_ASSERT_IF(!existingLoadingInfo, !existingEntry); + if (existingLoadingInfo) { + if (existingEntry) { + entry = existingEntry; + } else { + MOZ_ASSERT(!existingLoadingInfo->mLoadIsFromSessionHistory); + + SessionHistoryEntry::LoadingEntry* loadingEntry = + SessionHistoryEntry::GetByLoadId(existingLoadingInfo->mLoadId); + MOZ_LOG(gSHLog, LogLevel::Verbose, + ("SHEntry::GetByLoadId(%" PRIu64 ") -> %p", + existingLoadingInfo->mLoadId, entry.get())); + if (!loadingEntry) { + return nullptr; + } + entry = loadingEntry->mEntry; + } + + // If the entry was updated, update also the LoadingSessionHistoryInfo. + UniquePtr<LoadingSessionHistoryInfo> lshi = + MakeUnique<LoadingSessionHistoryInfo>(entry, existingLoadingInfo); + aLoadState->SetLoadingSessionHistoryInfo(std::move(lshi)); + existingLoadingInfo = aLoadState->GetLoadingSessionHistoryInfo(); + Unused << SetHistoryEntryCount(entry->BCHistoryLength()); + } else if (aLoadState->LoadType() == LOAD_REFRESH && + !ShouldAddEntryForRefresh(aLoadState->URI(), + aLoadState->PostDataStream()) && + mActiveEntry) { + entry = mActiveEntry; + } else { + entry = new SessionHistoryEntry(aLoadState, aChannel); + if (IsTop()) { + // Only top level pages care about Get/SetPersist. + entry->SetPersist( + nsDocShell::ShouldAddToSessionHistory(aLoadState->URI(), aChannel)); + } else if (mActiveEntry || !mLoadingEntries.IsEmpty()) { + entry->SetIsSubFrame(true); + } + entry->SetDocshellID(GetHistoryID()); + entry->SetIsDynamicallyAdded(CreatedDynamically()); + entry->SetForInitialLoad(true); + } + MOZ_DIAGNOSTIC_ASSERT(entry); + + UniquePtr<LoadingSessionHistoryInfo> loadingInfo; + if (existingLoadingInfo) { + loadingInfo = MakeUnique<LoadingSessionHistoryInfo>(*existingLoadingInfo); + } else { + loadingInfo = MakeUnique<LoadingSessionHistoryInfo>(entry); + mLoadingEntries.AppendElement( + LoadingSessionHistoryEntry{loadingInfo->mLoadId, entry}); + } + + MOZ_ASSERT(SessionHistoryEntry::GetByLoadId(loadingInfo->mLoadId)->mEntry == + entry); + + return loadingInfo; +} + +UniquePtr<LoadingSessionHistoryInfo> +CanonicalBrowsingContext::ReplaceLoadingSessionHistoryEntryForLoad( + LoadingSessionHistoryInfo* aInfo, nsIChannel* aNewChannel) { + MOZ_ASSERT(aInfo); + MOZ_ASSERT(aNewChannel); + + SessionHistoryInfo newInfo = SessionHistoryInfo( + aNewChannel, aInfo->mInfo.LoadType(), + aInfo->mInfo.GetPartitionedPrincipalToInherit(), aInfo->mInfo.GetCsp()); + + for (size_t i = 0; i < mLoadingEntries.Length(); ++i) { + if (mLoadingEntries[i].mLoadId == aInfo->mLoadId) { + RefPtr<SessionHistoryEntry> loadingEntry = mLoadingEntries[i].mEntry; + loadingEntry->SetInfo(&newInfo); + + if (IsTop()) { + // Only top level pages care about Get/SetPersist. + nsCOMPtr<nsIURI> uri; + aNewChannel->GetURI(getter_AddRefs(uri)); + loadingEntry->SetPersist( + nsDocShell::ShouldAddToSessionHistory(uri, aNewChannel)); + } else { + loadingEntry->SetIsSubFrame(aInfo->mInfo.IsSubFrame()); + } + loadingEntry->SetDocshellID(GetHistoryID()); + loadingEntry->SetIsDynamicallyAdded(CreatedDynamically()); + return MakeUnique<LoadingSessionHistoryInfo>(loadingEntry, aInfo); + } + } + return nullptr; +} + +using PrintPromise = CanonicalBrowsingContext::PrintPromise; +#ifdef NS_PRINTING +class PrintListenerAdapter final : public nsIWebProgressListener { + public: + explicit PrintListenerAdapter(PrintPromise::Private* aPromise) + : mPromise(aPromise) {} + + NS_DECL_ISUPPORTS + + // NS_DECL_NSIWEBPROGRESSLISTENER + NS_IMETHOD OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + uint32_t aStateFlags, nsresult aStatus) override { + if (aStateFlags & nsIWebProgressListener::STATE_STOP && + aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT && mPromise) { + mPromise->Resolve(true, __func__); + mPromise = nullptr; + } + return NS_OK; + } + NS_IMETHOD OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) override { + if (aStatus != NS_OK && mPromise) { + mPromise->Reject(aStatus, __func__); + mPromise = nullptr; + } + return NS_OK; + } + NS_IMETHOD OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) override { + return NS_OK; + } + NS_IMETHOD OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aLocation, + uint32_t aFlags) override { + return NS_OK; + } + NS_IMETHOD OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) override { + return NS_OK; + } + NS_IMETHOD OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) override { + return NS_OK; + } + + private: + ~PrintListenerAdapter() = default; + + RefPtr<PrintPromise::Private> mPromise; +}; + +NS_IMPL_ISUPPORTS(PrintListenerAdapter, nsIWebProgressListener) +#endif + +already_AddRefed<Promise> CanonicalBrowsingContext::PrintJS( + nsIPrintSettings* aPrintSettings, ErrorResult& aRv) { + RefPtr<Promise> promise = Promise::Create(GetIncumbentGlobal(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return promise.forget(); + } + + Print(aPrintSettings) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [promise](bool) { promise->MaybeResolveWithUndefined(); }, + [promise](nsresult aResult) { promise->MaybeReject(aResult); }); + return promise.forget(); +} + +RefPtr<PrintPromise> CanonicalBrowsingContext::Print( + nsIPrintSettings* aPrintSettings) { +#ifndef NS_PRINTING + return PrintPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); +#else + + auto promise = MakeRefPtr<PrintPromise::Private>(__func__); + auto listener = MakeRefPtr<PrintListenerAdapter>(promise); + if (IsInProcess()) { + RefPtr<nsGlobalWindowOuter> outerWindow = + nsGlobalWindowOuter::Cast(GetDOMWindow()); + if (NS_WARN_IF(!outerWindow)) { + promise->Reject(NS_ERROR_FAILURE, __func__); + return promise; + } + + ErrorResult rv; + outerWindow->Print(aPrintSettings, + /* aRemotePrintJob = */ nullptr, listener, + /* aDocShellToCloneInto = */ nullptr, + nsGlobalWindowOuter::IsPreview::No, + nsGlobalWindowOuter::IsForWindowDotPrint::No, + /* aPrintPreviewCallback = */ nullptr, rv); + if (rv.Failed()) { + promise->Reject(rv.StealNSResult(), __func__); + } + return promise; + } + + auto* browserParent = GetBrowserParent(); + if (NS_WARN_IF(!browserParent)) { + promise->Reject(NS_ERROR_FAILURE, __func__); + return promise; + } + + nsCOMPtr<nsIPrintSettingsService> printSettingsSvc = + do_GetService("@mozilla.org/gfx/printsettings-service;1"); + if (NS_WARN_IF(!printSettingsSvc)) { + promise->Reject(NS_ERROR_FAILURE, __func__); + return promise; + } + + nsresult rv; + nsCOMPtr<nsIPrintSettings> printSettings = aPrintSettings; + if (!printSettings) { + rv = + printSettingsSvc->CreateNewPrintSettings(getter_AddRefs(printSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject(rv, __func__); + return promise; + } + } + + embedding::PrintData printData; + rv = printSettingsSvc->SerializeToPrintData(printSettings, &printData); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject(rv, __func__); + return promise; + } + + layout::RemotePrintJobParent* remotePrintJob = + new layout::RemotePrintJobParent(printSettings); + printData.remotePrintJob() = + browserParent->Manager()->SendPRemotePrintJobConstructor(remotePrintJob); + + if (listener) { + remotePrintJob->RegisterListener(listener); + } + + if (NS_WARN_IF(!browserParent->SendPrint(this, printData))) { + promise->Reject(NS_ERROR_FAILURE, __func__); + } + return promise.forget(); +#endif +} + +void CanonicalBrowsingContext::CallOnAllTopDescendants( + const std::function<mozilla::CallState(CanonicalBrowsingContext*)>& + aCallback) { +#ifdef DEBUG + RefPtr<CanonicalBrowsingContext> parent = GetParentCrossChromeBoundary(); + MOZ_ASSERT(!parent, "Should only call on top chrome BC"); +#endif + + nsTArray<RefPtr<BrowsingContextGroup>> groups; + BrowsingContextGroup::GetAllGroups(groups); + for (auto& browsingContextGroup : groups) { + for (auto& bc : browsingContextGroup->Toplevels()) { + if (bc == this) { + // Cannot be a descendent of myself so skip. + continue; + } + + RefPtr<CanonicalBrowsingContext> top = + bc->Canonical()->TopCrossChromeBoundary(); + if (top == this) { + if (aCallback(bc->Canonical()) == CallState::Stop) { + return; + } + } + } + } +} + +void CanonicalBrowsingContext::SessionHistoryCommit( + uint64_t aLoadId, const nsID& aChangeID, uint32_t aLoadType, bool aPersist, + bool aCloneEntryChildren, bool aChannelExpired, uint32_t aCacheKey) { + MOZ_LOG(gSHLog, LogLevel::Verbose, + ("CanonicalBrowsingContext::SessionHistoryCommit %p %" PRIu64, this, + aLoadId)); + MOZ_ASSERT(aLoadId != UINT64_MAX, + "Must not send special about:blank loadinfo to parent."); + for (size_t i = 0; i < mLoadingEntries.Length(); ++i) { + if (mLoadingEntries[i].mLoadId == aLoadId) { + nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory()); + if (!shistory) { + SessionHistoryEntry::RemoveLoadId(aLoadId); + mLoadingEntries.RemoveElementAt(i); + return; + } + + RefPtr<SessionHistoryEntry> newActiveEntry = mLoadingEntries[i].mEntry; + if (aCacheKey != 0) { + newActiveEntry->SetCacheKey(aCacheKey); + } + + if (aChannelExpired) { + newActiveEntry->SharedInfo()->mExpired = true; + } + + bool loadFromSessionHistory = !newActiveEntry->ForInitialLoad(); + newActiveEntry->SetForInitialLoad(false); + SessionHistoryEntry::RemoveLoadId(aLoadId); + mLoadingEntries.RemoveElementAt(i); + + int32_t indexOfHistoryLoad = -1; + if (loadFromSessionHistory) { + nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(newActiveEntry); + indexOfHistoryLoad = shistory->GetIndexOfEntry(root); + if (indexOfHistoryLoad < 0) { + // Entry has been removed from the session history. + return; + } + } + + CallerWillNotifyHistoryIndexAndLengthChanges caller(shistory); + + // If there is a name in the new entry, clear the name of all contiguous + // entries. This is for https://html.spec.whatwg.org/#history-traversal + // Step 4.4.2. + nsAutoString nameOfNewEntry; + newActiveEntry->GetName(nameOfNewEntry); + if (!nameOfNewEntry.IsEmpty()) { + nsSHistory::WalkContiguousEntries( + newActiveEntry, + [](nsISHEntry* aEntry) { aEntry->SetName(EmptyString()); }); + } + + bool addEntry = ShouldUpdateSessionHistory(aLoadType); + if (IsTop()) { + if (mActiveEntry && !mActiveEntry->GetFrameLoader()) { + bool sharesDocument = true; + mActiveEntry->SharesDocumentWith(newActiveEntry, &sharesDocument); + if (!sharesDocument) { + // If the old page won't be in the bfcache, + // clear the dynamic entries. + RemoveDynEntriesFromActiveSessionHistoryEntry(); + } + } + + if (LOAD_TYPE_HAS_FLAGS(aLoadType, + nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY)) { + // Replace the current entry with the new entry. + int32_t index = shistory->GetIndexForReplace(); + + // If we're trying to replace an inexistant shistory entry then we + // should append instead. + addEntry = index < 0; + if (!addEntry) { + shistory->ReplaceEntry(index, newActiveEntry); + } + mActiveEntry = newActiveEntry; + } else if (LOAD_TYPE_HAS_FLAGS( + aLoadType, nsIWebNavigation::LOAD_FLAGS_IS_REFRESH) && + !ShouldAddEntryForRefresh(newActiveEntry) && mActiveEntry) { + addEntry = false; + mActiveEntry->ReplaceWith(*newActiveEntry); + } else { + mActiveEntry = newActiveEntry; + } + + if (loadFromSessionHistory) { + // XXX Synchronize browsing context tree and session history tree? + shistory->InternalSetRequestedIndex(indexOfHistoryLoad); + shistory->UpdateIndex(); + + if (IsTop()) { + mActiveEntry->SetWireframe(Nothing()); + } + } else if (addEntry) { + shistory->AddEntry(mActiveEntry, aPersist); + shistory->InternalSetRequestedIndex(-1); + } + } else { + // FIXME The old implementations adds it to the parent's mLSHE if there + // is one, need to figure out if that makes sense here (peterv + // doesn't think it would). + if (loadFromSessionHistory) { + if (mActiveEntry) { + // mActiveEntry is null if we're loading iframes from session + // history while also parent page is loading from session history. + // In that case there isn't anything to sync. + mActiveEntry->SyncTreesForSubframeNavigation(newActiveEntry, Top(), + this); + } + mActiveEntry = newActiveEntry; + + shistory->InternalSetRequestedIndex(indexOfHistoryLoad); + // FIXME UpdateIndex() here may update index too early (but even the + // old implementation seems to have similar issues). + shistory->UpdateIndex(); + } else if (addEntry) { + if (mActiveEntry) { + if (LOAD_TYPE_HAS_FLAGS( + aLoadType, nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY) || + (LOAD_TYPE_HAS_FLAGS(aLoadType, + nsIWebNavigation::LOAD_FLAGS_IS_REFRESH) && + !ShouldAddEntryForRefresh(newActiveEntry))) { + // FIXME We need to make sure that when we create the info we + // make a copy of the shared state. + mActiveEntry->ReplaceWith(*newActiveEntry); + } else { + // AddChildSHEntryHelper does update the index of the session + // history! + shistory->AddChildSHEntryHelper(mActiveEntry, newActiveEntry, + Top(), aCloneEntryChildren); + mActiveEntry = newActiveEntry; + } + } else { + SessionHistoryEntry* parentEntry = GetParent()->mActiveEntry; + // XXX What should happen if parent doesn't have mActiveEntry? + // Or can that even happen ever? + if (parentEntry) { + mActiveEntry = newActiveEntry; + // FIXME Using IsInProcess for aUseRemoteSubframes isn't quite + // right, but aUseRemoteSubframes should be going away. + parentEntry->AddChild( + mActiveEntry, + CreatedDynamically() ? -1 : GetParent()->IndexOf(this), + IsInProcess()); + } + } + shistory->InternalSetRequestedIndex(-1); + } + } + + ResetSHEntryHasUserInteractionCache(); + + HistoryCommitIndexAndLength(aChangeID, caller); + + shistory->LogHistory(); + + return; + } + // XXX Should the loading entries before [i] be removed? + } + // FIXME Should we throw an error if we don't find an entry for + // aSessionHistoryEntryId? +} + +already_AddRefed<nsDocShellLoadState> CanonicalBrowsingContext::CreateLoadInfo( + SessionHistoryEntry* aEntry) { + const SessionHistoryInfo& info = aEntry->Info(); + RefPtr<nsDocShellLoadState> loadState(new nsDocShellLoadState(info.GetURI())); + info.FillLoadInfo(*loadState); + UniquePtr<LoadingSessionHistoryInfo> loadingInfo; + loadingInfo = MakeUnique<LoadingSessionHistoryInfo>(aEntry); + mLoadingEntries.AppendElement( + LoadingSessionHistoryEntry{loadingInfo->mLoadId, aEntry}); + loadState->SetLoadingSessionHistoryInfo(std::move(loadingInfo)); + + return loadState.forget(); +} + +void CanonicalBrowsingContext::NotifyOnHistoryReload( + bool aForceReload, bool& aCanReload, + Maybe<NotNull<RefPtr<nsDocShellLoadState>>>& aLoadState, + Maybe<bool>& aReloadActiveEntry) { + MOZ_DIAGNOSTIC_ASSERT(!aLoadState); + + aCanReload = true; + nsISHistory* shistory = GetSessionHistory(); + NS_ENSURE_TRUE_VOID(shistory); + + shistory->NotifyOnHistoryReload(&aCanReload); + if (!aCanReload) { + return; + } + + if (mActiveEntry) { + aLoadState.emplace(WrapMovingNotNull(RefPtr{CreateLoadInfo(mActiveEntry)})); + aReloadActiveEntry.emplace(true); + if (aForceReload) { + shistory->RemoveFrameEntries(mActiveEntry); + } + } else if (!mLoadingEntries.IsEmpty()) { + const LoadingSessionHistoryEntry& loadingEntry = + mLoadingEntries.LastElement(); + uint64_t loadId = loadingEntry.mLoadId; + aLoadState.emplace( + WrapMovingNotNull(RefPtr{CreateLoadInfo(loadingEntry.mEntry)})); + aReloadActiveEntry.emplace(false); + if (aForceReload) { + SessionHistoryEntry::LoadingEntry* entry = + SessionHistoryEntry::GetByLoadId(loadId); + if (entry) { + shistory->RemoveFrameEntries(entry->mEntry); + } + } + } + + if (aLoadState) { + // Use 0 as the offset, since aLoadState will be be used for reload. + aLoadState.ref()->SetLoadIsFromSessionHistory(0, + aReloadActiveEntry.value()); + } + // If we don't have an active entry and we don't have a loading entry then + // the nsDocShell will create a load state based on its document. +} + +void CanonicalBrowsingContext::SetActiveSessionHistoryEntry( + const Maybe<nsPoint>& aPreviousScrollPos, SessionHistoryInfo* aInfo, + uint32_t aLoadType, uint32_t aUpdatedCacheKey, const nsID& aChangeID) { + nsISHistory* shistory = GetSessionHistory(); + if (!shistory) { + return; + } + CallerWillNotifyHistoryIndexAndLengthChanges caller(shistory); + + RefPtr<SessionHistoryEntry> oldActiveEntry = mActiveEntry; + if (aPreviousScrollPos.isSome() && oldActiveEntry) { + oldActiveEntry->SetScrollPosition(aPreviousScrollPos.ref().x, + aPreviousScrollPos.ref().y); + } + mActiveEntry = new SessionHistoryEntry(aInfo); + mActiveEntry->SetDocshellID(GetHistoryID()); + mActiveEntry->AdoptBFCacheEntry(oldActiveEntry); + if (aUpdatedCacheKey != 0) { + mActiveEntry->SharedInfo()->mCacheKey = aUpdatedCacheKey; + } + + if (IsTop()) { + Maybe<int32_t> previousEntryIndex, loadedEntryIndex; + shistory->AddToRootSessionHistory( + true, oldActiveEntry, this, mActiveEntry, aLoadType, + nsDocShell::ShouldAddToSessionHistory(aInfo->GetURI(), nullptr), + &previousEntryIndex, &loadedEntryIndex); + } else { + if (oldActiveEntry) { + shistory->AddChildSHEntryHelper(oldActiveEntry, mActiveEntry, Top(), + true); + } else if (GetParent() && GetParent()->mActiveEntry) { + GetParent()->mActiveEntry->AddChild( + mActiveEntry, CreatedDynamically() ? -1 : GetParent()->IndexOf(this), + UseRemoteSubframes()); + } + } + + ResetSHEntryHasUserInteractionCache(); + + shistory->InternalSetRequestedIndex(-1); + + // FIXME Need to do the equivalent of EvictContentViewersOrReplaceEntry. + HistoryCommitIndexAndLength(aChangeID, caller); + + static_cast<nsSHistory*>(shistory)->LogHistory(); +} + +void CanonicalBrowsingContext::ReplaceActiveSessionHistoryEntry( + SessionHistoryInfo* aInfo) { + if (!mActiveEntry) { + return; + } + + mActiveEntry->SetInfo(aInfo); + // Notify children of the update + nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory()); + if (shistory) { + shistory->NotifyOnHistoryReplaceEntry(); + shistory->UpdateRootBrowsingContextState(); + } + + ResetSHEntryHasUserInteractionCache(); + + if (IsTop()) { + mActiveEntry->SetWireframe(Nothing()); + } + + // FIXME Need to do the equivalent of EvictContentViewersOrReplaceEntry. +} + +void CanonicalBrowsingContext::RemoveDynEntriesFromActiveSessionHistoryEntry() { + nsISHistory* shistory = GetSessionHistory(); + // In theory shistory can be null here if the method is called right after + // CanonicalBrowsingContext::ReplacedBy call. + NS_ENSURE_TRUE_VOID(shistory); + nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(mActiveEntry); + shistory->RemoveDynEntries(shistory->GetIndexOfEntry(root), mActiveEntry); +} + +void CanonicalBrowsingContext::RemoveFromSessionHistory(const nsID& aChangeID) { + nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory()); + if (shistory) { + CallerWillNotifyHistoryIndexAndLengthChanges caller(shistory); + nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(mActiveEntry); + bool didRemove; + AutoTArray<nsID, 16> ids({GetHistoryID()}); + shistory->RemoveEntries(ids, shistory->GetIndexOfEntry(root), &didRemove); + if (didRemove) { + RefPtr<BrowsingContext> rootBC = shistory->GetBrowsingContext(); + if (rootBC) { + if (!rootBC->IsInProcess()) { + if (ContentParent* cp = rootBC->Canonical()->GetContentParent()) { + Unused << cp->SendDispatchLocationChangeEvent(rootBC); + } + } else if (rootBC->GetDocShell()) { + rootBC->GetDocShell()->DispatchLocationChangeEvent(); + } + } + } + HistoryCommitIndexAndLength(aChangeID, caller); + } +} + +Maybe<int32_t> CanonicalBrowsingContext::HistoryGo( + int32_t aOffset, uint64_t aHistoryEpoch, bool aRequireUserInteraction, + bool aUserActivation, Maybe<ContentParentId> aContentId) { + if (aRequireUserInteraction && aOffset != -1 && aOffset != 1) { + NS_ERROR( + "aRequireUserInteraction may only be used with an offset of -1 or 1"); + return Nothing(); + } + + nsSHistory* shistory = static_cast<nsSHistory*>(GetSessionHistory()); + if (!shistory) { + return Nothing(); + } + + CheckedInt<int32_t> index = shistory->GetRequestedIndex() >= 0 + ? shistory->GetRequestedIndex() + : shistory->Index(); + MOZ_LOG(gSHLog, LogLevel::Debug, + ("HistoryGo(%d->%d) epoch %" PRIu64 "/id %" PRIu64, aOffset, + (index + aOffset).value(), aHistoryEpoch, + (uint64_t)(aContentId.isSome() ? aContentId.value() : 0))); + + while (true) { + index += aOffset; + if (!index.isValid()) { + MOZ_LOG(gSHLog, LogLevel::Debug, ("Invalid index")); + return Nothing(); + } + + // Check for user interaction if desired, except for the first and last + // history entries. We compare with >= to account for the case where + // aOffset >= length. + if (!aRequireUserInteraction || index.value() >= shistory->Length() - 1 || + index.value() <= 0) { + break; + } + if (shistory->HasUserInteractionAtIndex(index.value())) { + break; + } + } + + // Implement aborting additional history navigations from within the same + // event spin of the content process. + + uint64_t epoch; + bool sameEpoch = false; + Maybe<ContentParentId> id; + shistory->GetEpoch(epoch, id); + + if (aContentId == id && epoch >= aHistoryEpoch) { + sameEpoch = true; + MOZ_LOG(gSHLog, LogLevel::Debug, ("Same epoch/id")); + } + // Don't update the epoch until we know if the target index is valid + + // GoToIndex checks that index is >= 0 and < length. + nsTArray<nsSHistory::LoadEntryResult> loadResults; + nsresult rv = shistory->GotoIndex(index.value(), loadResults, sameEpoch, + aOffset == 0, aUserActivation); + if (NS_FAILED(rv)) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("Dropping HistoryGo - bad index or same epoch (not in same doc)")); + return Nothing(); + } + if (epoch < aHistoryEpoch || aContentId != id) { + MOZ_LOG(gSHLog, LogLevel::Debug, ("Set epoch")); + shistory->SetEpoch(aHistoryEpoch, aContentId); + } + int32_t requestedIndex = shistory->GetRequestedIndex(); + nsSHistory::LoadURIs(loadResults); + return Some(requestedIndex); +} + +JSObject* CanonicalBrowsingContext::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return CanonicalBrowsingContext_Binding::Wrap(aCx, this, aGivenProto); +} + +void CanonicalBrowsingContext::DispatchWheelZoomChange(bool aIncrease) { + Element* element = Top()->GetEmbedderElement(); + if (!element) { + return; + } + + auto event = aIncrease ? u"DoZoomEnlargeBy10"_ns : u"DoZoomReduceBy10"_ns; + auto dispatcher = MakeRefPtr<AsyncEventDispatcher>( + element, event, CanBubble::eYes, ChromeOnlyDispatch::eYes); + dispatcher->PostDOMEvent(); +} + +void CanonicalBrowsingContext::CanonicalDiscard() { + if (mTabMediaController) { + mTabMediaController->Shutdown(); + mTabMediaController = nullptr; + } + + if (mCurrentLoad) { + mCurrentLoad->Cancel(NS_BINDING_ABORTED, + "CanonicalBrowsingContext::CanonicalDiscard"_ns); + } + + if (mWebProgress) { + RefPtr<BrowsingContextWebProgress> progress = mWebProgress; + progress->ContextDiscarded(); + } + + if (IsTop()) { + BackgroundSessionStorageManager::RemoveManager(Id()); + } + + CancelSessionStoreUpdate(); + + if (UsePrivateBrowsing() && EverAttached() && IsContent()) { + DecreasePrivateCount(); + } +} + +void CanonicalBrowsingContext::CanonicalAttach() { + if (UsePrivateBrowsing() && IsContent()) { + IncreasePrivateCount(); + } +} + +void CanonicalBrowsingContext::AddPendingDiscard() { + MOZ_ASSERT(!mFullyDiscarded); + mPendingDiscards++; +} + +void CanonicalBrowsingContext::RemovePendingDiscard() { + mPendingDiscards--; + if (!mPendingDiscards) { + mFullyDiscarded = true; + auto listeners = std::move(mFullyDiscardedListeners); + for (const auto& listener : listeners) { + listener(Id()); + } + } +} + +void CanonicalBrowsingContext::AddFinalDiscardListener( + std::function<void(uint64_t)>&& aListener) { + if (mFullyDiscarded) { + aListener(Id()); + return; + } + mFullyDiscardedListeners.AppendElement(std::move(aListener)); +} + +void CanonicalBrowsingContext::SetForceAppWindowActive(bool aForceActive, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(IsChrome()); + MOZ_DIAGNOSTIC_ASSERT(IsTop()); + if (!IsChrome() || !IsTop()) { + return aRv.ThrowNotAllowedError( + "You shouldn't need to force this BrowsingContext to be active, use " + ".isActive instead"); + } + if (mForceAppWindowActive == aForceActive) { + return; + } + mForceAppWindowActive = aForceActive; + RecomputeAppWindowVisibility(); +} + +void CanonicalBrowsingContext::RecomputeAppWindowVisibility() { + MOZ_RELEASE_ASSERT(IsChrome()); + MOZ_RELEASE_ASSERT(IsTop()); + + const bool isActive = [&] { + if (ForceAppWindowActive()) { + return true; + } + auto* docShell = GetDocShell(); + if (NS_WARN_IF(!docShell)) { + return false; + } + nsCOMPtr<nsIWidget> widget; + nsDocShell::Cast(docShell)->GetMainWidget(getter_AddRefs(widget)); + if (NS_WARN_IF(!widget)) { + return false; + } + if (widget->IsFullyOccluded() || + widget->SizeMode() == nsSizeMode_Minimized) { + return false; + } + return true; + }(); + SetIsActive(isActive, IgnoreErrors()); +} + +void CanonicalBrowsingContext::AdjustPrivateBrowsingCount( + bool aPrivateBrowsing) { + if (IsDiscarded() || !EverAttached() || IsChrome()) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(aPrivateBrowsing == UsePrivateBrowsing()); + if (aPrivateBrowsing) { + IncreasePrivateCount(); + } else { + DecreasePrivateCount(); + } +} + +void CanonicalBrowsingContext::NotifyStartDelayedAutoplayMedia() { + WindowContext* windowContext = GetCurrentWindowContext(); + if (!windowContext) { + return; + } + + // As this function would only be called when user click the play icon on the + // tab bar. That's clear user intent to play, so gesture activate the window + // context so that the block-autoplay logic allows the media to autoplay. + windowContext->NotifyUserGestureActivation(); + AUTOPLAY_LOG("NotifyStartDelayedAutoplayMedia for chrome bc 0x%08" PRIx64, + Id()); + StartDelayedAutoplayMediaComponents(); + // Notfiy all content browsing contexts which are related with the canonical + // browsing content tree to start delayed autoplay media. + + Group()->EachParent([&](ContentParent* aParent) { + Unused << aParent->SendStartDelayedAutoplayMediaComponents(this); + }); +} + +void CanonicalBrowsingContext::NotifyMediaMutedChanged(bool aMuted, + ErrorResult& aRv) { + MOZ_ASSERT(!GetParent(), + "Notify media mute change on non top-level context!"); + SetMuted(aMuted, aRv); +} + +uint32_t CanonicalBrowsingContext::CountSiteOrigins( + GlobalObject& aGlobal, + const Sequence<OwningNonNull<BrowsingContext>>& aRoots) { + nsTHashSet<nsCString> uniqueSiteOrigins; + + for (const auto& root : aRoots) { + root->PreOrderWalk([&](BrowsingContext* aContext) { + WindowGlobalParent* windowGlobalParent = + aContext->Canonical()->GetCurrentWindowGlobal(); + if (windowGlobalParent) { + nsIPrincipal* documentPrincipal = + windowGlobalParent->DocumentPrincipal(); + + bool isContentPrincipal = documentPrincipal->GetIsContentPrincipal(); + if (isContentPrincipal) { + nsCString siteOrigin; + documentPrincipal->GetSiteOrigin(siteOrigin); + uniqueSiteOrigins.Insert(siteOrigin); + } + } + }); + } + + return uniqueSiteOrigins.Count(); +} + +/* static */ +bool CanonicalBrowsingContext::IsPrivateBrowsingActive() { + return gNumberOfPrivateContexts > 0; +} + +void CanonicalBrowsingContext::UpdateMediaControlAction( + const MediaControlAction& aAction) { + if (IsDiscarded()) { + return; + } + ContentMediaControlKeyHandler::HandleMediaControlAction(this, aAction); + Group()->EachParent([&](ContentParent* aParent) { + Unused << aParent->SendUpdateMediaControlAction(this, aAction); + }); +} + +void CanonicalBrowsingContext::LoadURI(nsIURI* aURI, + const LoadURIOptions& aOptions, + ErrorResult& aError) { + RefPtr<nsDocShellLoadState> loadState; + nsresult rv = nsDocShellLoadState::CreateFromLoadURIOptions( + this, aURI, aOptions, getter_AddRefs(loadState)); + MOZ_ASSERT(rv != NS_ERROR_MALFORMED_URI); + + if (NS_FAILED(rv)) { + aError.Throw(rv); + return; + } + + LoadURI(loadState, true); +} + +void CanonicalBrowsingContext::FixupAndLoadURIString( + const nsAString& aURI, const LoadURIOptions& aOptions, + ErrorResult& aError) { + RefPtr<nsDocShellLoadState> loadState; + nsresult rv = nsDocShellLoadState::CreateFromLoadURIOptions( + this, aURI, aOptions, getter_AddRefs(loadState)); + + if (rv == NS_ERROR_MALFORMED_URI) { + DisplayLoadError(aURI); + return; + } + + if (NS_FAILED(rv)) { + aError.Throw(rv); + return; + } + + LoadURI(loadState, true); +} + +void CanonicalBrowsingContext::GoBack( + const Optional<int32_t>& aCancelContentJSEpoch, + bool aRequireUserInteraction, bool aUserActivation) { + if (IsDiscarded()) { + return; + } + + // Stop any known network loads if necessary. + if (mCurrentLoad) { + mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns); + } + + if (RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell())) { + if (aCancelContentJSEpoch.WasPassed()) { + docShell->SetCancelContentJSEpoch(aCancelContentJSEpoch.Value()); + } + docShell->GoBack(aRequireUserInteraction, aUserActivation); + } else if (ContentParent* cp = GetContentParent()) { + Maybe<int32_t> cancelContentJSEpoch; + if (aCancelContentJSEpoch.WasPassed()) { + cancelContentJSEpoch = Some(aCancelContentJSEpoch.Value()); + } + Unused << cp->SendGoBack(this, cancelContentJSEpoch, + aRequireUserInteraction, aUserActivation); + } +} +void CanonicalBrowsingContext::GoForward( + const Optional<int32_t>& aCancelContentJSEpoch, + bool aRequireUserInteraction, bool aUserActivation) { + if (IsDiscarded()) { + return; + } + + // Stop any known network loads if necessary. + if (mCurrentLoad) { + mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns); + } + + if (RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell())) { + if (aCancelContentJSEpoch.WasPassed()) { + docShell->SetCancelContentJSEpoch(aCancelContentJSEpoch.Value()); + } + docShell->GoForward(aRequireUserInteraction, aUserActivation); + } else if (ContentParent* cp = GetContentParent()) { + Maybe<int32_t> cancelContentJSEpoch; + if (aCancelContentJSEpoch.WasPassed()) { + cancelContentJSEpoch.emplace(aCancelContentJSEpoch.Value()); + } + Unused << cp->SendGoForward(this, cancelContentJSEpoch, + aRequireUserInteraction, aUserActivation); + } +} +void CanonicalBrowsingContext::GoToIndex( + int32_t aIndex, const Optional<int32_t>& aCancelContentJSEpoch, + bool aUserActivation) { + if (IsDiscarded()) { + return; + } + + // Stop any known network loads if necessary. + if (mCurrentLoad) { + mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns); + } + + if (RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell())) { + if (aCancelContentJSEpoch.WasPassed()) { + docShell->SetCancelContentJSEpoch(aCancelContentJSEpoch.Value()); + } + docShell->GotoIndex(aIndex, aUserActivation); + } else if (ContentParent* cp = GetContentParent()) { + Maybe<int32_t> cancelContentJSEpoch; + if (aCancelContentJSEpoch.WasPassed()) { + cancelContentJSEpoch.emplace(aCancelContentJSEpoch.Value()); + } + Unused << cp->SendGoToIndex(this, aIndex, cancelContentJSEpoch, + aUserActivation); + } +} +void CanonicalBrowsingContext::Reload(uint32_t aReloadFlags) { + if (IsDiscarded()) { + return; + } + + // Stop any known network loads if necessary. + if (mCurrentLoad) { + mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns); + } + + if (RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell())) { + docShell->Reload(aReloadFlags); + } else if (ContentParent* cp = GetContentParent()) { + Unused << cp->SendReload(this, aReloadFlags); + } +} + +void CanonicalBrowsingContext::Stop(uint32_t aStopFlags) { + if (IsDiscarded()) { + return; + } + + // Stop any known network loads if necessary. + if (mCurrentLoad && (aStopFlags & nsIWebNavigation::STOP_NETWORK)) { + mCurrentLoad->Cancel(NS_BINDING_ABORTED, + "CanonicalBrowsingContext::Stop"_ns); + } + + // Ask the docshell to stop to handle loads that haven't + // yet reached here, as well as non-network activity. + if (auto* docShell = nsDocShell::Cast(GetDocShell())) { + docShell->Stop(aStopFlags); + } else if (ContentParent* cp = GetContentParent()) { + Unused << cp->SendStopLoad(this, aStopFlags); + } +} + +void CanonicalBrowsingContext::PendingRemotenessChange::ProcessLaunched() { + if (!mPromise) { + return; + } + + if (mContentParent) { + // If our new content process is still unloading from a previous process + // switch, wait for that unload to complete before continuing. + auto found = mTarget->FindUnloadingHost(mContentParent->ChildID()); + if (found != mTarget->mUnloadingHosts.end()) { + found->mCallbacks.AppendElement( + [self = RefPtr{this}]() { self->ProcessReady(); }); + return; + } + } + + ProcessReady(); +} + +void CanonicalBrowsingContext::PendingRemotenessChange::ProcessReady() { + if (!mPromise) { + return; + } + + MOZ_ASSERT(!mProcessReady); + mProcessReady = true; + MaybeFinish(); +} + +void CanonicalBrowsingContext::PendingRemotenessChange::MaybeFinish() { + if (!mPromise) { + return; + } + + if (!mProcessReady || mWaitingForPrepareToChange) { + return; + } + + // If this BrowsingContext is embedded within the parent process, perform the + // process switch directly. + nsresult rv = mTarget->IsTopContent() ? FinishTopContent() : FinishSubframe(); + if (NS_FAILED(rv)) { + NS_WARNING("Error finishing PendingRemotenessChange!"); + Cancel(rv); + } else { + Clear(); + } +} + +// Logic for finishing a toplevel process change embedded within the parent +// process. Due to frontend integration the logic differs substantially from +// subframe process switches, and is handled separately. +nsresult CanonicalBrowsingContext::PendingRemotenessChange::FinishTopContent() { + MOZ_DIAGNOSTIC_ASSERT(mTarget->IsTop(), + "We shouldn't be trying to change the remoteness of " + "non-remote iframes"); + + // Abort if our ContentParent died while process switching. + if (mContentParent && NS_WARN_IF(mContentParent->IsShuttingDown())) { + return NS_ERROR_FAILURE; + } + + // While process switching, we need to check if any of our ancestors are + // discarded or no longer current, in which case the process switch needs to + // be aborted. + RefPtr<CanonicalBrowsingContext> target(mTarget); + if (target->IsDiscarded() || !target->AncestorsAreCurrent()) { + return NS_ERROR_FAILURE; + } + + Element* browserElement = target->GetEmbedderElement(); + if (!browserElement) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIBrowser> browser = browserElement->AsBrowser(); + if (!browser) { + return NS_ERROR_FAILURE; + } + + RefPtr<nsFrameLoaderOwner> frameLoaderOwner = do_QueryObject(browserElement); + MOZ_RELEASE_ASSERT(frameLoaderOwner, + "embedder browser must be nsFrameLoaderOwner"); + + // If we're process switching a browsing context in private browsing + // mode we might decrease the private browsing count to '0', which + // would make us fire "last-pb-context-exited" and drop the private + // session. To prevent that we artificially increment the number of + // private browsing contexts with '1' until the process switch is done. + bool usePrivateBrowsing = mTarget->UsePrivateBrowsing(); + if (usePrivateBrowsing) { + IncreasePrivateCount(); + } + + auto restorePrivateCount = MakeScopeExit([usePrivateBrowsing]() { + if (usePrivateBrowsing) { + DecreasePrivateCount(); + } + }); + + // Tell frontend code that this browser element is about to change process. + nsresult rv = browser->BeforeChangeRemoteness(); + if (NS_FAILED(rv)) { + return rv; + } + + // Some frontend code checks the value of the `remote` attribute on the + // browser to determine if it is remote, so update the value. + browserElement->SetAttr(kNameSpaceID_None, nsGkAtoms::remote, + mContentParent ? u"true"_ns : u"false"_ns, + /* notify */ true); + + // The process has been created, hand off to nsFrameLoaderOwner to finish + // the process switch. + ErrorResult error; + frameLoaderOwner->ChangeRemotenessToProcess(mContentParent, mOptions, + mSpecificGroup, error); + if (error.Failed()) { + return error.StealNSResult(); + } + + // Tell frontend the load is done. + bool loadResumed = false; + rv = browser->FinishChangeRemoteness(mPendingSwitchId, &loadResumed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We did it! The process switch is complete. + RefPtr<nsFrameLoader> frameLoader = frameLoaderOwner->GetFrameLoader(); + RefPtr<BrowserParent> newBrowser = frameLoader->GetBrowserParent(); + if (!newBrowser) { + if (mContentParent) { + // Failed to create the BrowserParent somehow! Abort the process switch + // attempt. + return NS_ERROR_UNEXPECTED; + } + + if (!loadResumed) { + RefPtr<nsDocShell> newDocShell = frameLoader->GetDocShell(error); + if (error.Failed()) { + return error.StealNSResult(); + } + + rv = newDocShell->ResumeRedirectedLoad(mPendingSwitchId, + /* aHistoryIndex */ -1); + if (NS_FAILED(rv)) { + return rv; + } + } + } else if (!loadResumed) { + newBrowser->ResumeLoad(mPendingSwitchId); + } + + mPromise->Resolve(newBrowser, __func__); + return NS_OK; +} + +nsresult CanonicalBrowsingContext::PendingRemotenessChange::FinishSubframe() { + MOZ_DIAGNOSTIC_ASSERT(!mOptions.mReplaceBrowsingContext, + "Cannot replace BC for subframe"); + MOZ_DIAGNOSTIC_ASSERT(!mTarget->IsTop()); + + // While process switching, we need to check if any of our ancestors are + // discarded or no longer current, in which case the process switch needs to + // be aborted. + RefPtr<CanonicalBrowsingContext> target(mTarget); + if (target->IsDiscarded() || !target->AncestorsAreCurrent()) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!mContentParent)) { + return NS_ERROR_FAILURE; + } + + RefPtr<WindowGlobalParent> embedderWindow = target->GetParentWindowContext(); + if (NS_WARN_IF(!embedderWindow) || NS_WARN_IF(!embedderWindow->CanSend())) { + return NS_ERROR_FAILURE; + } + + RefPtr<BrowserParent> embedderBrowser = embedderWindow->GetBrowserParent(); + if (NS_WARN_IF(!embedderBrowser)) { + return NS_ERROR_FAILURE; + } + + // If we're creating a new remote browser, and the host process is already + // dead, abort the process switch. + if (mContentParent != embedderBrowser->Manager() && + NS_WARN_IF(mContentParent->IsShuttingDown())) { + return NS_ERROR_FAILURE; + } + + RefPtr<BrowserParent> oldBrowser = target->GetBrowserParent(); + target->SetCurrentBrowserParent(nullptr); + + // If we were in a remote frame, trigger unloading of the remote window. The + // previous BrowserParent is registered in `mUnloadingHosts` and will only be + // cleared when the BrowserParent is fully destroyed. + bool wasRemote = oldBrowser && oldBrowser->GetBrowsingContext() == target; + if (wasRemote) { + MOZ_DIAGNOSTIC_ASSERT(oldBrowser != embedderBrowser); + MOZ_DIAGNOSTIC_ASSERT(oldBrowser->IsDestroyed() || + oldBrowser->GetBrowserBridgeParent()); + + // `oldBrowser` will clear the `UnloadingHost` status once the actor has + // been destroyed. + if (oldBrowser->CanSend()) { + target->StartUnloadingHost(oldBrowser->Manager()->ChildID()); + Unused << oldBrowser->SendWillChangeProcess(); + oldBrowser->Destroy(); + } + } + + // Update which process is considered the current owner + target->SetOwnerProcessId(mContentParent->ChildID()); + + // If we're switching from remote to local, we don't need to create a + // BrowserBridge, and can instead perform the switch directly. + if (mContentParent == embedderBrowser->Manager()) { + MOZ_DIAGNOSTIC_ASSERT( + mPendingSwitchId, + "We always have a PendingSwitchId, except for print-preview loads, " + "which will never perform a process-switch to being in-process with " + "their embedder"); + MOZ_DIAGNOSTIC_ASSERT(wasRemote, + "Attempt to process-switch from local to local?"); + + target->SetCurrentBrowserParent(embedderBrowser); + Unused << embedderWindow->SendMakeFrameLocal(target, mPendingSwitchId); + mPromise->Resolve(embedderBrowser, __func__); + return NS_OK; + } + + // The BrowsingContext will be remote, either as an already-remote frame + // changing processes, or as a local frame becoming remote. Construct a new + // BrowserBridgeParent to host the remote content. + target->SetCurrentBrowserParent(nullptr); + + MOZ_DIAGNOSTIC_ASSERT(target->UseRemoteTabs() && target->UseRemoteSubframes(), + "Not supported without fission"); + uint32_t chromeFlags = nsIWebBrowserChrome::CHROME_REMOTE_WINDOW | + nsIWebBrowserChrome::CHROME_FISSION_WINDOW; + if (target->UsePrivateBrowsing()) { + chromeFlags |= nsIWebBrowserChrome::CHROME_PRIVATE_WINDOW; + } + + nsCOMPtr<nsIPrincipal> initialPrincipal = + NullPrincipal::Create(target->OriginAttributesRef()); + WindowGlobalInit windowInit = + WindowGlobalActor::AboutBlankInitializer(target, initialPrincipal); + + // Create and initialize our new BrowserBridgeParent. + TabId tabId(nsContentUtils::GenerateTabId()); + RefPtr<BrowserBridgeParent> bridge = new BrowserBridgeParent(); + nsresult rv = bridge->InitWithProcess(embedderBrowser, mContentParent, + windowInit, chromeFlags, tabId); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If we've already destroyed our previous document, make a best-effort + // attempt to recover from this failure and show the crashed tab UI. We only + // do this in the previously-remote case, as previously in-process frames + // will have their navigation cancelled, and will remain visible. + if (wasRemote) { + target->ShowSubframeCrashedUI(oldBrowser->GetBrowserBridgeParent()); + } + return rv; + } + + // Tell the embedder process a remoteness change is in-process. When this is + // acknowledged, reset the in-flight ID if it used to be an in-process load. + RefPtr<BrowserParent> newBrowser = bridge->GetBrowserParent(); + { + // If we weren't remote, mark our embedder window browser as unloading until + // our embedder process has acked our MakeFrameRemote message. + Maybe<uint64_t> clearChildID; + if (!wasRemote) { + clearChildID = Some(embedderBrowser->Manager()->ChildID()); + target->StartUnloadingHost(*clearChildID); + } + auto callback = [target, clearChildID](auto&&) { + if (clearChildID) { + target->ClearUnloadingHost(*clearChildID); + } + }; + + ManagedEndpoint<PBrowserBridgeChild> endpoint = + embedderBrowser->OpenPBrowserBridgeEndpoint(bridge); + MOZ_DIAGNOSTIC_ASSERT(endpoint.IsValid()); + embedderWindow->SendMakeFrameRemote(target, std::move(endpoint), tabId, + newBrowser->GetLayersId(), callback, + callback); + } + + // Resume the pending load in our new process. + if (mPendingSwitchId) { + newBrowser->ResumeLoad(mPendingSwitchId); + } + + // We did it! The process switch is complete. + mPromise->Resolve(newBrowser, __func__); + return NS_OK; +} + +void CanonicalBrowsingContext::PendingRemotenessChange::Cancel(nsresult aRv) { + if (!mPromise) { + return; + } + + mPromise->Reject(aRv, __func__); + Clear(); +} + +void CanonicalBrowsingContext::PendingRemotenessChange::Clear() { + // Make sure we don't die while we're doing cleanup. + RefPtr<PendingRemotenessChange> kungFuDeathGrip(this); + if (mTarget) { + MOZ_DIAGNOSTIC_ASSERT(mTarget->mPendingRemotenessChange == this); + mTarget->mPendingRemotenessChange = nullptr; + } + + // When this PendingRemotenessChange was created, it was given a + // `mContentParent`. + if (mContentParent) { + mContentParent->RemoveKeepAlive(); + mContentParent = nullptr; + } + + // If we were given a specific group, stop keeping that group alive manually. + if (mSpecificGroup) { + mSpecificGroup->RemoveKeepAlive(); + mSpecificGroup = nullptr; + } + + mPromise = nullptr; + mTarget = nullptr; +} + +CanonicalBrowsingContext::PendingRemotenessChange::PendingRemotenessChange( + CanonicalBrowsingContext* aTarget, RemotenessPromise::Private* aPromise, + uint64_t aPendingSwitchId, const NavigationIsolationOptions& aOptions) + : mTarget(aTarget), + mPromise(aPromise), + mPendingSwitchId(aPendingSwitchId), + mOptions(aOptions) {} + +CanonicalBrowsingContext::PendingRemotenessChange::~PendingRemotenessChange() { + MOZ_ASSERT(!mPromise && !mTarget && !mContentParent && !mSpecificGroup, + "should've already been Cancel() or Complete()-ed"); +} + +BrowserParent* CanonicalBrowsingContext::GetBrowserParent() const { + return mCurrentBrowserParent; +} + +void CanonicalBrowsingContext::SetCurrentBrowserParent( + BrowserParent* aBrowserParent) { + MOZ_DIAGNOSTIC_ASSERT(!mCurrentBrowserParent || !aBrowserParent, + "BrowsingContext already has a current BrowserParent!"); + MOZ_DIAGNOSTIC_ASSERT_IF(aBrowserParent, aBrowserParent->CanSend()); + MOZ_DIAGNOSTIC_ASSERT_IF(aBrowserParent, + aBrowserParent->Manager()->ChildID() == mProcessId); + + // BrowserParent must either be directly for this BrowsingContext, or the + // manager out our embedder WindowGlobal. + MOZ_DIAGNOSTIC_ASSERT_IF( + aBrowserParent && aBrowserParent->GetBrowsingContext() != this, + GetParentWindowContext() && + GetParentWindowContext()->Manager() == aBrowserParent); + + mCurrentBrowserParent = aBrowserParent; +} + +RefPtr<CanonicalBrowsingContext::RemotenessPromise> +CanonicalBrowsingContext::ChangeRemoteness( + const NavigationIsolationOptions& aOptions, uint64_t aPendingSwitchId) { + MOZ_DIAGNOSTIC_ASSERT(IsContent(), + "cannot change the process of chrome contexts"); + MOZ_DIAGNOSTIC_ASSERT( + IsTop() == IsEmbeddedInProcess(0), + "toplevel content must be embedded in the parent process"); + MOZ_DIAGNOSTIC_ASSERT(!aOptions.mReplaceBrowsingContext || IsTop(), + "Cannot replace BrowsingContext for subframes"); + MOZ_DIAGNOSTIC_ASSERT( + aOptions.mSpecificGroupId == 0 || aOptions.mReplaceBrowsingContext, + "Cannot specify group ID unless replacing BC"); + MOZ_DIAGNOSTIC_ASSERT(aPendingSwitchId || !IsTop(), + "Should always have aPendingSwitchId for top-level " + "frames"); + + if (!AncestorsAreCurrent()) { + NS_WARNING("An ancestor context is no longer current"); + return RemotenessPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + // Ensure our embedder hasn't been destroyed or asked to shutdown already. + RefPtr<WindowGlobalParent> embedderWindowGlobal = GetEmbedderWindowGlobal(); + if (!embedderWindowGlobal) { + NS_WARNING("Non-embedded BrowsingContext"); + return RemotenessPromise::CreateAndReject(NS_ERROR_UNEXPECTED, __func__); + } + + if (!embedderWindowGlobal->CanSend()) { + NS_WARNING("Embedder already been destroyed."); + return RemotenessPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); + } + + RefPtr<BrowserParent> embedderBrowser = + embedderWindowGlobal->GetBrowserParent(); + if (embedderBrowser && embedderBrowser->Manager()->IsShuttingDown()) { + NS_WARNING("Embedder already asked to shutdown."); + return RemotenessPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); + } + + if (aOptions.mRemoteType.IsEmpty() && (!IsTop() || !GetEmbedderElement())) { + NS_WARNING("Cannot load non-remote subframes"); + return RemotenessPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + // Cancel ongoing remoteness changes. + if (mPendingRemotenessChange) { + mPendingRemotenessChange->Cancel(NS_ERROR_ABORT); + MOZ_DIAGNOSTIC_ASSERT(!mPendingRemotenessChange, "Should have cleared"); + } + + auto promise = MakeRefPtr<RemotenessPromise::Private>(__func__); + promise->UseDirectTaskDispatch(__func__); + + RefPtr<PendingRemotenessChange> change = + new PendingRemotenessChange(this, promise, aPendingSwitchId, aOptions); + mPendingRemotenessChange = change; + + // If a specific BrowsingContextGroup ID was specified for this load, make + // sure to keep it alive until the process switch is completed. + if (aOptions.mSpecificGroupId) { + change->mSpecificGroup = + BrowsingContextGroup::GetOrCreate(aOptions.mSpecificGroupId); + change->mSpecificGroup->AddKeepAlive(); + } + + // Call `prepareToChangeRemoteness` in parallel with starting a new process + // for <browser> loads. + if (IsTop() && GetEmbedderElement()) { + nsCOMPtr<nsIBrowser> browser = GetEmbedderElement()->AsBrowser(); + if (!browser) { + change->Cancel(NS_ERROR_FAILURE); + return promise.forget(); + } + + RefPtr<Promise> blocker; + nsresult rv = browser->PrepareToChangeRemoteness(getter_AddRefs(blocker)); + if (NS_FAILED(rv)) { + change->Cancel(rv); + return promise.forget(); + } + + // Mark prepareToChange as unresolved, and wait for it to become resolved. + if (blocker && blocker->State() != Promise::PromiseState::Resolved) { + change->mWaitingForPrepareToChange = true; + RefPtr<DomPromiseListener> listener = new DomPromiseListener( + [change](JSContext* aCx, JS::Handle<JS::Value> aValue) { + change->mWaitingForPrepareToChange = false; + change->MaybeFinish(); + }, + [change](nsresult aRv) { change->Cancel(aRv); }); + blocker->AppendNativeHandler(listener); + } + } + + // Switching a subframe to be local within it's embedding process. + if (embedderBrowser && + aOptions.mRemoteType == embedderBrowser->Manager()->GetRemoteType()) { + MOZ_DIAGNOSTIC_ASSERT( + aPendingSwitchId, + "We always have a PendingSwitchId, except for print-preview loads, " + "which will never perform a process-switch to being in-process with " + "their embedder"); + MOZ_DIAGNOSTIC_ASSERT(!aOptions.mReplaceBrowsingContext); + MOZ_DIAGNOSTIC_ASSERT(!aOptions.mRemoteType.IsEmpty()); + MOZ_DIAGNOSTIC_ASSERT(!change->mWaitingForPrepareToChange); + MOZ_DIAGNOSTIC_ASSERT(!change->mSpecificGroup); + + // Switching to local, so we don't need to create a new process, and will + // instead use our embedder process. + change->mContentParent = embedderBrowser->Manager(); + change->mContentParent->AddKeepAlive(); + change->ProcessLaunched(); + return promise.forget(); + } + + // Switching to the parent process. + if (aOptions.mRemoteType.IsEmpty()) { + change->ProcessLaunched(); + return promise.forget(); + } + + // If we're aiming to end up in a new process of the same type as our old + // process, and then putting our previous document in the BFCache, try to stay + // in the same process to avoid creating new processes unnecessarily. + RefPtr<ContentParent> existingProcess = GetContentParent(); + if (existingProcess && !existingProcess->IsShuttingDown() && + aOptions.mReplaceBrowsingContext && + aOptions.mRemoteType == existingProcess->GetRemoteType()) { + change->mContentParent = existingProcess; + change->mContentParent->AddKeepAlive(); + change->ProcessLaunched(); + return promise.forget(); + } + + // Try to predict which BrowsingContextGroup will be used for the final load + // in this BrowsingContext. This has to be accurate if switching into an + // existing group, as it will control what pool of processes will be used + // for process selection. + // + // It's _technically_ OK to provide a group here if we're actually going to + // switch into a brand new group, though it's sub-optimal, as it can + // restrict the set of processes we're using. + BrowsingContextGroup* finalGroup = + aOptions.mReplaceBrowsingContext ? change->mSpecificGroup.get() : Group(); + + bool preferUsed = + StaticPrefs::browser_tabs_remote_subframesPreferUsed() && !IsTop(); + + change->mContentParent = ContentParent::GetNewOrUsedLaunchingBrowserProcess( + /* aRemoteType = */ aOptions.mRemoteType, + /* aGroup = */ finalGroup, + /* aPriority = */ hal::PROCESS_PRIORITY_FOREGROUND, + /* aPreferUsed = */ preferUsed); + if (!change->mContentParent) { + change->Cancel(NS_ERROR_FAILURE); + return promise.forget(); + } + + // Add a KeepAlive used by this ContentParent, which will be cleared when + // the change is complete. This should prevent the process dying before + // we're ready to use it. + change->mContentParent->AddKeepAlive(); + if (change->mContentParent->IsLaunching()) { + change->mContentParent->WaitForLaunchAsync()->Then( + GetMainThreadSerialEventTarget(), __func__, + [change](ContentParent*) { change->ProcessLaunched(); }, + [change]() { change->Cancel(NS_ERROR_FAILURE); }); + } else { + change->ProcessLaunched(); + } + return promise.forget(); +} + +void CanonicalBrowsingContext::MaybeSetPermanentKey(Element* aEmbedder) { + MOZ_DIAGNOSTIC_ASSERT(IsTop()); + + if (aEmbedder) { + if (nsCOMPtr<nsIBrowser> browser = aEmbedder->AsBrowser()) { + JS::Rooted<JS::Value> key(RootingCx()); + if (NS_SUCCEEDED(browser->GetPermanentKey(&key)) && key.isObject()) { + mPermanentKey = key; + } + } + } +} + +MediaController* CanonicalBrowsingContext::GetMediaController() { + // We would only create one media controller per tab, so accessing the + // controller via the top-level browsing context. + if (GetParent()) { + return Cast(Top())->GetMediaController(); + } + + MOZ_ASSERT(!GetParent(), + "Must access the controller from the top-level browsing context!"); + // Only content browsing context can create media controller, we won't create + // controller for chrome document, such as the browser UI. + if (!mTabMediaController && !IsDiscarded() && IsContent()) { + mTabMediaController = new MediaController(Id()); + } + return mTabMediaController; +} + +bool CanonicalBrowsingContext::HasCreatedMediaController() const { + return !!mTabMediaController; +} + +bool CanonicalBrowsingContext::SupportsLoadingInParent( + nsDocShellLoadState* aLoadState, uint64_t* aOuterWindowId) { + // We currently don't support initiating loads in the parent when they are + // watched by devtools. This is because devtools tracks loads using content + // process notifications, which happens after the load is initiated in this + // case. Devtools clears all prior requests when it detects a new navigation, + // so it drops the main document load that happened here. + if (WatchedByDevTools()) { + return false; + } + + // Session-history-in-parent implementation relies currently on getting a + // round trip through a child process. + if (aLoadState->LoadIsFromSessionHistory()) { + return false; + } + + // DocumentChannel currently only supports connecting channels into the + // content process, so we can only support schemes that will always be loaded + // there for now. Restrict to just http(s) for simplicity. + if (!net::SchemeIsHTTP(aLoadState->URI()) && + !net::SchemeIsHTTPS(aLoadState->URI())) { + return false; + } + + if (WindowGlobalParent* global = GetCurrentWindowGlobal()) { + nsCOMPtr<nsIURI> currentURI = global->GetDocumentURI(); + if (currentURI) { + bool newURIHasRef = false; + aLoadState->URI()->GetHasRef(&newURIHasRef); + bool equalsExceptRef = false; + aLoadState->URI()->EqualsExceptRef(currentURI, &equalsExceptRef); + + if (equalsExceptRef && newURIHasRef) { + // This navigation is same-doc WRT the current one, we should pass it + // down to the docshell to be handled. + return false; + } + } + // If the current document has a beforeunload listener, then we need to + // start the load in that process after we fire the event. + if (global->HasBeforeUnload()) { + return false; + } + + *aOuterWindowId = global->OuterWindowId(); + } + return true; +} + +bool CanonicalBrowsingContext::LoadInParent(nsDocShellLoadState* aLoadState, + bool aSetNavigating) { + // We currently only support starting loads directly from the + // CanonicalBrowsingContext for top-level BCs. + // We currently only support starting loads directly from the + // CanonicalBrowsingContext for top-level BCs. + if (!IsTopContent() || !GetContentParent() || + !StaticPrefs::browser_tabs_documentchannel_parent_controlled()) { + return false; + } + + uint64_t outerWindowId = 0; + if (!SupportsLoadingInParent(aLoadState, &outerWindowId)) { + return false; + } + + MOZ_ASSERT(!net::SchemeIsJavascript(aLoadState->URI())); + + MOZ_ALWAYS_SUCCEEDS( + SetParentInitiatedNavigationEpoch(++gParentInitiatedNavigationEpoch)); + // Note: If successful, this will recurse into StartDocumentLoad and + // set mCurrentLoad to the DocumentLoadListener instance created. + // Ideally in the future we will only start loads from here, and we can + // just set this directly instead. + return net::DocumentLoadListener::LoadInParent(this, aLoadState, + aSetNavigating); +} + +bool CanonicalBrowsingContext::AttemptSpeculativeLoadInParent( + nsDocShellLoadState* aLoadState) { + // We currently only support starting loads directly from the + // CanonicalBrowsingContext for top-level BCs. + // We currently only support starting loads directly from the + // CanonicalBrowsingContext for top-level BCs. + if (!IsTopContent() || !GetContentParent() || + (StaticPrefs::browser_tabs_documentchannel_parent_controlled())) { + return false; + } + + uint64_t outerWindowId = 0; + if (!SupportsLoadingInParent(aLoadState, &outerWindowId)) { + return false; + } + + // If we successfully open the DocumentChannel, then it'll register + // itself using aLoadIdentifier and be kept alive until it completes + // loading. + return net::DocumentLoadListener::SpeculativeLoadInParent(this, aLoadState); +} + +bool CanonicalBrowsingContext::StartDocumentLoad( + net::DocumentLoadListener* aLoad) { + // If we're controlling loads from the parent, then starting a new load means + // that we need to cancel any existing ones. + if (StaticPrefs::browser_tabs_documentchannel_parent_controlled() && + mCurrentLoad) { + // Make sure we are not loading a javascript URI. + MOZ_ASSERT(!aLoad->IsLoadingJSURI()); + + // If we want to do a download, don't cancel the current navigation. + if (!aLoad->IsDownload()) { + mCurrentLoad->Cancel(NS_BINDING_CANCELLED_OLD_LOAD, ""_ns); + } + } + mCurrentLoad = aLoad; + + if (NS_FAILED(SetCurrentLoadIdentifier(Some(aLoad->GetLoadIdentifier())))) { + mCurrentLoad = nullptr; + return false; + } + + return true; +} + +void CanonicalBrowsingContext::EndDocumentLoad(bool aContinueNavigating) { + mCurrentLoad = nullptr; + + if (!aContinueNavigating) { + // Resetting the current load identifier on a discarded context + // has no effect when a document load has finished. + Unused << SetCurrentLoadIdentifier(Nothing()); + } +} + +already_AddRefed<nsIURI> CanonicalBrowsingContext::GetCurrentURI() const { + nsCOMPtr<nsIURI> currentURI; + if (nsIDocShell* docShell = GetDocShell()) { + MOZ_ALWAYS_SUCCEEDS( + nsDocShell::Cast(docShell)->GetCurrentURI(getter_AddRefs(currentURI))); + } else { + currentURI = mCurrentRemoteURI; + } + return currentURI.forget(); +} + +void CanonicalBrowsingContext::SetCurrentRemoteURI(nsIURI* aCurrentRemoteURI) { + MOZ_ASSERT(!GetDocShell()); + mCurrentRemoteURI = aCurrentRemoteURI; +} + +void CanonicalBrowsingContext::ResetSHEntryHasUserInteractionCache() { + WindowContext* topWc = GetTopWindowContext(); + if (topWc && !topWc->IsDiscarded()) { + MOZ_ALWAYS_SUCCEEDS(topWc->SetSHEntryHasUserInteraction(false)); + } +} + +void CanonicalBrowsingContext::HistoryCommitIndexAndLength() { + nsID changeID = {}; + CallerWillNotifyHistoryIndexAndLengthChanges caller(nullptr); + HistoryCommitIndexAndLength(changeID, caller); +} +void CanonicalBrowsingContext::HistoryCommitIndexAndLength( + const nsID& aChangeID, + const CallerWillNotifyHistoryIndexAndLengthChanges& aProofOfCaller) { + if (!IsTop()) { + Cast(Top())->HistoryCommitIndexAndLength(aChangeID, aProofOfCaller); + return; + } + + nsISHistory* shistory = GetSessionHistory(); + if (!shistory) { + return; + } + int32_t index = 0; + shistory->GetIndex(&index); + int32_t length = shistory->GetCount(); + + GetChildSessionHistory()->SetIndexAndLength(index, length, aChangeID); + + shistory->EvictOutOfRangeContentViewers(index); + + Group()->EachParent([&](ContentParent* aParent) { + Unused << aParent->SendHistoryCommitIndexAndLength(this, index, length, + aChangeID); + }); +} + +void CanonicalBrowsingContext::SynchronizeLayoutHistoryState() { + if (mActiveEntry) { + if (IsInProcess()) { + nsIDocShell* docShell = GetDocShell(); + if (docShell) { + docShell->PersistLayoutHistoryState(); + + nsCOMPtr<nsILayoutHistoryState> state; + docShell->GetLayoutHistoryState(getter_AddRefs(state)); + if (state) { + mActiveEntry->SetLayoutHistoryState(state); + } + } + } else if (ContentParent* cp = GetContentParent()) { + cp->SendGetLayoutHistoryState(this)->Then( + GetCurrentSerialEventTarget(), __func__, + [activeEntry = mActiveEntry]( + const std::tuple<RefPtr<nsILayoutHistoryState>, Maybe<Wireframe>>& + aResult) { + if (std::get<0>(aResult)) { + activeEntry->SetLayoutHistoryState(std::get<0>(aResult)); + } + if (std::get<1>(aResult)) { + activeEntry->SetWireframe(std::get<1>(aResult)); + } + }, + []() {}); + } + } +} + +void CanonicalBrowsingContext::ResetScalingZoom() { + // This currently only ever gets called in the parent process, and we + // pass the message on to the WindowGlobalChild for the rootmost browsing + // context. + if (WindowGlobalParent* topWindow = GetTopWindowContext()) { + Unused << topWindow->SendResetScalingZoom(); + } +} + +void CanonicalBrowsingContext::SetRestoreData(SessionStoreRestoreData* aData, + ErrorResult& aError) { + MOZ_DIAGNOSTIC_ASSERT(aData); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + RefPtr<Promise> promise = Promise::Create(global, aError); + if (aError.Failed()) { + return; + } + + if (NS_WARN_IF(NS_FAILED(SetHasRestoreData(true)))) { + aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mRestoreState = new RestoreState(); + mRestoreState->mData = aData; + mRestoreState->mPromise = promise; +} + +already_AddRefed<Promise> CanonicalBrowsingContext::GetRestorePromise() { + if (mRestoreState) { + return do_AddRef(mRestoreState->mPromise); + } + return nullptr; +} + +void CanonicalBrowsingContext::ClearRestoreState() { + if (!mRestoreState) { + MOZ_DIAGNOSTIC_ASSERT(!GetHasRestoreData()); + return; + } + if (mRestoreState->mPromise) { + mRestoreState->mPromise->MaybeRejectWithUndefined(); + } + mRestoreState = nullptr; + MOZ_ALWAYS_SUCCEEDS(SetHasRestoreData(false)); +} + +void CanonicalBrowsingContext::RequestRestoreTabContent( + WindowGlobalParent* aWindow) { + MOZ_DIAGNOSTIC_ASSERT(IsTop()); + + if (IsDiscarded() || !mRestoreState || !mRestoreState->mData) { + return; + } + + CanonicalBrowsingContext* context = aWindow->GetBrowsingContext(); + MOZ_DIAGNOSTIC_ASSERT(!context->IsDiscarded()); + + RefPtr<SessionStoreRestoreData> data = + mRestoreState->mData->FindDataForChild(context); + + if (context->IsTop()) { + MOZ_DIAGNOSTIC_ASSERT(context == this); + + // We need to wait until the appropriate load event has fired before we + // can "complete" the restore process, so if we're holding an empty data + // object, just resolve the promise immediately. + if (mRestoreState->mData->IsEmpty()) { + MOZ_DIAGNOSTIC_ASSERT(!data || data->IsEmpty()); + mRestoreState->Resolve(); + ClearRestoreState(); + return; + } + + // Since we're following load event order, we'll only arrive here for a + // toplevel context after we've already sent down data for all child frames, + // so it's safe to clear this reference now. The completion callback below + // relies on the mData field being null to determine if all requests have + // been sent out. + mRestoreState->ClearData(); + MOZ_ALWAYS_SUCCEEDS(SetHasRestoreData(false)); + } + + if (data && !data->IsEmpty()) { + auto onTabRestoreComplete = [self = RefPtr{this}, + state = RefPtr{mRestoreState}](auto) { + state->mResolves++; + if (!state->mData && state->mRequests == state->mResolves) { + state->Resolve(); + if (state == self->mRestoreState) { + self->ClearRestoreState(); + } + } + }; + + mRestoreState->mRequests++; + + if (data->CanRestoreInto(aWindow->GetDocumentURI())) { + if (!aWindow->IsInProcess()) { + aWindow->SendRestoreTabContent(WrapNotNull(data.get()), + onTabRestoreComplete, + onTabRestoreComplete); + return; + } + data->RestoreInto(context); + } + + // This must be called both when we're doing an in-process restore, and when + // we didn't do a restore at all due to a URL mismatch. + onTabRestoreComplete(true); + } +} + +void CanonicalBrowsingContext::RestoreState::Resolve() { + MOZ_DIAGNOSTIC_ASSERT(mPromise); + mPromise->MaybeResolveWithUndefined(); + mPromise = nullptr; +} + +nsresult CanonicalBrowsingContext::WriteSessionStorageToSessionStore( + const nsTArray<SSCacheCopy>& aSesssionStorage, uint32_t aEpoch) { + nsCOMPtr<nsISessionStoreFunctions> funcs = do_ImportESModule( + "resource://gre/modules/SessionStoreFunctions.sys.mjs", fallible); + if (!funcs) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(funcs); + AutoJSAPI jsapi; + if (!jsapi.Init(wrapped->GetJSObjectGlobal())) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JS::Value> key(jsapi.cx(), Top()->PermanentKey()); + + Record<nsCString, Record<nsString, nsString>> storage; + JS::Rooted<JS::Value> update(jsapi.cx()); + + if (!aSesssionStorage.IsEmpty()) { + SessionStoreUtils::ConstructSessionStorageValues(this, aSesssionStorage, + storage); + if (!ToJSValue(jsapi.cx(), storage, &update)) { + return NS_ERROR_FAILURE; + } + } else { + update.setNull(); + } + + return funcs->UpdateSessionStoreForStorage(Top()->GetEmbedderElement(), this, + key, aEpoch, update); +} + +void CanonicalBrowsingContext::UpdateSessionStoreSessionStorage( + const std::function<void()>& aDone) { + if (!StaticPrefs::browser_sessionstore_collect_session_storage_AtStartup()) { + aDone(); + return; + } + + using DataPromise = BackgroundSessionStorageManager::DataPromise; + BackgroundSessionStorageManager::GetData( + this, StaticPrefs::browser_sessionstore_dom_storage_limit(), + /* aClearSessionStoreTimer = */ true) + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr{this}, aDone, epoch = GetSessionStoreEpoch()]( + const DataPromise::ResolveOrRejectValue& valueList) { + if (valueList.IsResolve()) { + self->WriteSessionStorageToSessionStore( + valueList.ResolveValue(), epoch); + } + aDone(); + }); +} + +/* static */ +void CanonicalBrowsingContext::UpdateSessionStoreForStorage( + uint64_t aBrowsingContextId) { + RefPtr<CanonicalBrowsingContext> browsingContext = Get(aBrowsingContextId); + + if (!browsingContext) { + return; + } + + browsingContext->UpdateSessionStoreSessionStorage([]() {}); +} + +void CanonicalBrowsingContext::MaybeScheduleSessionStoreUpdate() { + if (!StaticPrefs::browser_sessionstore_platform_collection_AtStartup()) { + return; + } + + if (!IsTop()) { + Top()->MaybeScheduleSessionStoreUpdate(); + return; + } + + if (IsInBFCache()) { + return; + } + + if (mSessionStoreSessionStorageUpdateTimer) { + return; + } + + if (!StaticPrefs::browser_sessionstore_debug_no_auto_updates()) { + auto result = NS_NewTimerWithFuncCallback( + [](nsITimer*, void* aClosure) { + auto* context = static_cast<CanonicalBrowsingContext*>(aClosure); + context->UpdateSessionStoreSessionStorage([]() {}); + }, + this, StaticPrefs::browser_sessionstore_interval(), + nsITimer::TYPE_ONE_SHOT, + "CanonicalBrowsingContext::MaybeScheduleSessionStoreUpdate"); + + if (result.isErr()) { + return; + } + + mSessionStoreSessionStorageUpdateTimer = result.unwrap(); + } +} + +void CanonicalBrowsingContext::CancelSessionStoreUpdate() { + if (mSessionStoreSessionStorageUpdateTimer) { + mSessionStoreSessionStorageUpdateTimer->Cancel(); + mSessionStoreSessionStorageUpdateTimer = nullptr; + } +} + +void CanonicalBrowsingContext::SetContainerFeaturePolicy( + FeaturePolicy* aContainerFeaturePolicy) { + mContainerFeaturePolicy = aContainerFeaturePolicy; + + if (WindowGlobalParent* current = GetCurrentWindowGlobal()) { + Unused << current->SendSetContainerFeaturePolicy(mContainerFeaturePolicy); + } +} + +void CanonicalBrowsingContext::SetCrossGroupOpenerId(uint64_t aOpenerId) { + MOZ_DIAGNOSTIC_ASSERT(IsTopContent()); + MOZ_DIAGNOSTIC_ASSERT(mCrossGroupOpenerId == 0, + "Can only set CrossGroupOpenerId once"); + mCrossGroupOpenerId = aOpenerId; +} + +void CanonicalBrowsingContext::SetCrossGroupOpener( + CanonicalBrowsingContext& aCrossGroupOpener, ErrorResult& aRv) { + if (!IsTopContent()) { + aRv.ThrowNotAllowedError( + "Can only set crossGroupOpener on toplevel content"); + return; + } + if (mCrossGroupOpenerId != 0) { + aRv.ThrowNotAllowedError("Can only set crossGroupOpener once"); + return; + } + + SetCrossGroupOpenerId(aCrossGroupOpener.Id()); +} + +auto CanonicalBrowsingContext::FindUnloadingHost(uint64_t aChildID) + -> nsTArray<UnloadingHost>::iterator { + return std::find_if( + mUnloadingHosts.begin(), mUnloadingHosts.end(), + [&](const auto& host) { return host.mChildID == aChildID; }); +} + +void CanonicalBrowsingContext::ClearUnloadingHost(uint64_t aChildID) { + // Notify any callbacks which were waiting for the host to finish unloading + // that it has. + auto found = FindUnloadingHost(aChildID); + if (found != mUnloadingHosts.end()) { + auto callbacks = std::move(found->mCallbacks); + mUnloadingHosts.RemoveElementAt(found); + for (const auto& callback : callbacks) { + callback(); + } + } +} + +void CanonicalBrowsingContext::StartUnloadingHost(uint64_t aChildID) { + MOZ_DIAGNOSTIC_ASSERT(FindUnloadingHost(aChildID) == mUnloadingHosts.end()); + mUnloadingHosts.AppendElement(UnloadingHost{aChildID, {}}); +} + +void CanonicalBrowsingContext::BrowserParentDestroyed( + BrowserParent* aBrowserParent, bool aAbnormalShutdown) { + ClearUnloadingHost(aBrowserParent->Manager()->ChildID()); + + // Handling specific to when the current BrowserParent has been destroyed. + if (mCurrentBrowserParent == aBrowserParent) { + mCurrentBrowserParent = nullptr; + + // If this BrowserParent is for a subframe, attempt to recover from a + // subframe crash by rendering the subframe crashed page in the embedding + // content. + if (aAbnormalShutdown) { + ShowSubframeCrashedUI(aBrowserParent->GetBrowserBridgeParent()); + } + } +} + +void CanonicalBrowsingContext::ShowSubframeCrashedUI( + BrowserBridgeParent* aBridge) { + if (!aBridge || IsDiscarded() || !aBridge->CanSend()) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(!aBridge->GetBrowsingContext() || + aBridge->GetBrowsingContext() == this); + + // There is no longer a current inner window within this + // BrowsingContext, update the `CurrentInnerWindowId` field to reflect + // this. + MOZ_ALWAYS_SUCCEEDS(SetCurrentInnerWindowId(0)); + + // The owning process will now be the embedder to render the subframe + // crashed page, switch ownership back over. + SetOwnerProcessId(aBridge->Manager()->Manager()->ChildID()); + SetCurrentBrowserParent(aBridge->Manager()); + + Unused << aBridge->SendSubFrameCrashed(); +} + +static void LogBFCacheBlockingForDoc(BrowsingContext* aBrowsingContext, + uint32_t aBFCacheCombo, bool aIsSubDoc) { + if (aIsSubDoc) { + nsAutoCString uri("[no uri]"); + nsCOMPtr<nsIURI> currentURI = + aBrowsingContext->Canonical()->GetCurrentURI(); + if (currentURI) { + uri = currentURI->GetSpecOrDefault(); + } + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, + (" ** Blocked for document %s", uri.get())); + } + if (aBFCacheCombo & BFCacheStatus::EVENT_HANDLING_SUPPRESSED) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, + (" * event handling suppression")); + } + if (aBFCacheCombo & BFCacheStatus::SUSPENDED) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * suspended Window")); + } + if (aBFCacheCombo & BFCacheStatus::UNLOAD_LISTENER) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * unload listener")); + } + if (aBFCacheCombo & BFCacheStatus::REQUEST) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * requests in the loadgroup")); + } + if (aBFCacheCombo & BFCacheStatus::ACTIVE_GET_USER_MEDIA) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * GetUserMedia")); + } + if (aBFCacheCombo & BFCacheStatus::ACTIVE_PEER_CONNECTION) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * PeerConnection")); + } + if (aBFCacheCombo & BFCacheStatus::CONTAINS_EME_CONTENT) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * EME content")); + } + if (aBFCacheCombo & BFCacheStatus::CONTAINS_MSE_CONTENT) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * MSE use")); + } + if (aBFCacheCombo & BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * Speech use")); + } + if (aBFCacheCombo & BFCacheStatus::HAS_USED_VR) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * used VR")); + } + if (aBFCacheCombo & BFCacheStatus::BEFOREUNLOAD_LISTENER) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * beforeunload listener")); + } + if (aBFCacheCombo & BFCacheStatus::ACTIVE_LOCK) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * has active Web Locks")); + } +} + +bool CanonicalBrowsingContext::AllowedInBFCache( + const Maybe<uint64_t>& aChannelId, nsIURI* aNewURI) { + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Debug))) { + nsAutoCString uri("[no uri]"); + nsCOMPtr<nsIURI> currentURI = GetCurrentURI(); + if (currentURI) { + uri = currentURI->GetSpecOrDefault(); + } + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, ("Checking %s", uri.get())); + } + + if (IsInProcess()) { + return false; + } + + uint32_t bfcacheCombo = 0; + if (mRestoreState) { + bfcacheCombo |= BFCacheStatus::RESTORING; + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * during session restore")); + } + + if (Group()->Toplevels().Length() > 1) { + bfcacheCombo |= BFCacheStatus::NOT_ONLY_TOPLEVEL_IN_BCG; + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, + (" * auxiliary BrowsingContexts")); + } + + // There are not a lot of about:* pages that are allowed to load in + // subframes, so it's OK to allow those few about:* pages enter BFCache. + MOZ_ASSERT(IsTop(), "Trying to put a non top level BC into BFCache"); + + WindowGlobalParent* wgp = GetCurrentWindowGlobal(); + if (wgp && wgp->GetDocumentURI()) { + nsCOMPtr<nsIURI> currentURI = wgp->GetDocumentURI(); + // Exempt about:* pages from bfcache, with the exception of about:blank + if (currentURI->SchemeIs("about") && + !currentURI->GetSpecOrDefault().EqualsLiteral("about:blank")) { + bfcacheCombo |= BFCacheStatus::ABOUT_PAGE; + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, (" * about:* page")); + } + + if (aNewURI) { + bool equalUri = false; + aNewURI->Equals(currentURI, &equalUri); + if (equalUri) { + // When loading the same uri, disable bfcache so that + // nsDocShell::OnNewURI transforms the load to LOAD_NORMAL_REPLACE. + return false; + } + } + } + + // For telemetry we're collecting all the flags for all the BCs hanging + // from this top-level BC. + PreOrderWalk([&](BrowsingContext* aBrowsingContext) { + WindowGlobalParent* wgp = + aBrowsingContext->Canonical()->GetCurrentWindowGlobal(); + uint32_t subDocBFCacheCombo = wgp ? wgp->GetBFCacheStatus() : 0; + if (wgp) { + const Maybe<uint64_t>& singleChannelId = wgp->GetSingleChannelId(); + if (singleChannelId.isSome()) { + if (singleChannelId.value() == 0 || aChannelId.isNothing() || + singleChannelId.value() != aChannelId.value()) { + subDocBFCacheCombo |= BFCacheStatus::REQUEST; + } + } + } + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Debug))) { + LogBFCacheBlockingForDoc(aBrowsingContext, subDocBFCacheCombo, + aBrowsingContext != this); + } + + bfcacheCombo |= subDocBFCacheCombo; + }); + + nsDocShell::ReportBFCacheComboTelemetry(bfcacheCombo); + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Debug))) { + nsAutoCString uri("[no uri]"); + nsCOMPtr<nsIURI> currentURI = GetCurrentURI(); + if (currentURI) { + uri = currentURI->GetSpecOrDefault(); + } + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, + (" +> %s %s be blocked from going into the BFCache", uri.get(), + bfcacheCombo == 0 ? "shouldn't" : "should")); + } + + if (StaticPrefs::docshell_shistory_bfcache_allow_unload_listeners()) { + bfcacheCombo &= ~BFCacheStatus::UNLOAD_LISTENER; + } + + return bfcacheCombo == 0; +} + +void CanonicalBrowsingContext::SetTouchEventsOverride( + dom::TouchEventsOverride aOverride, ErrorResult& aRv) { + SetTouchEventsOverrideInternal(aOverride, aRv); +} + +void CanonicalBrowsingContext::SetTargetTopLevelLinkClicksToBlank( + bool aTargetTopLevelLinkClicksToBlank, ErrorResult& aRv) { + SetTargetTopLevelLinkClicksToBlankInternal(aTargetTopLevelLinkClicksToBlank, + aRv); +} + +void CanonicalBrowsingContext::AddPageAwakeRequest() { + MOZ_ASSERT(IsTop()); + auto count = GetPageAwakeRequestCount(); + MOZ_ASSERT(count < UINT32_MAX); + Unused << SetPageAwakeRequestCount(++count); +} + +void CanonicalBrowsingContext::RemovePageAwakeRequest() { + MOZ_ASSERT(IsTop()); + auto count = GetPageAwakeRequestCount(); + MOZ_ASSERT(count > 0); + Unused << SetPageAwakeRequestCount(--count); +} + +void CanonicalBrowsingContext::CloneDocumentTreeInto( + CanonicalBrowsingContext* aSource, const nsACString& aRemoteType, + embedding::PrintData&& aPrintData) { + NavigationIsolationOptions options; + options.mRemoteType = aRemoteType; + + mClonePromise = + ChangeRemoteness(options, /* aPendingSwitchId = */ 0) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [source = MaybeDiscardedBrowsingContext{aSource}, + data = std::move(aPrintData)]( + BrowserParent* aBp) -> RefPtr<GenericNonExclusivePromise> { + RefPtr<BrowserBridgeParent> bridge = + aBp->GetBrowserBridgeParent(); + return aBp->SendCloneDocumentTreeIntoSelf(source, data) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [bridge]( + BrowserParent::CloneDocumentTreeIntoSelfPromise:: + ResolveOrRejectValue&& aValue) { + // We're cloning a remote iframe, so we created a + // BrowserBridge which makes us register an OOP load + // (see Document::OOPChildLoadStarted), even though + // this isn't a real load. We call + // SendMaybeFireEmbedderLoadEvents here so that we do + // register the end of the load (see + // Document::OOPChildLoadDone). + if (bridge) { + Unused << bridge->SendMaybeFireEmbedderLoadEvents( + EmbedderElementEventType::NoEvent); + } + if (aValue.IsResolve() && aValue.ResolveValue()) { + return GenericNonExclusivePromise::CreateAndResolve( + true, __func__); + } + return GenericNonExclusivePromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + }); + }, + [](nsresult aRv) -> RefPtr<GenericNonExclusivePromise> { + NS_WARNING( + nsPrintfCString("Remote clone failed: %x\n", unsigned(aRv)) + .get()); + return GenericNonExclusivePromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + }); + + mClonePromise->Then( + GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr{this}]() { self->mClonePromise = nullptr; }); +} + +bool CanonicalBrowsingContext::StartApzAutoscroll(float aAnchorX, + float aAnchorY, + nsViewID aScrollId, + uint32_t aPresShellId) { + nsCOMPtr<nsIWidget> widget; + mozilla::layers::LayersId layersId{0}; + + if (IsInProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> outer = GetDOMWindow(); + if (!outer) { + return false; + } + + widget = widget::WidgetUtils::DOMWindowToWidget(outer); + if (widget) { + layersId = widget->GetRootLayerTreeId(); + } + } else { + RefPtr<BrowserParent> parent = GetBrowserParent(); + if (!parent) { + return false; + } + + widget = parent->GetWidget(); + layersId = parent->GetLayersId(); + } + + if (!widget || !widget->AsyncPanZoomEnabled()) { + return false; + } + + // The anchor coordinates that are passed in are relative to the origin of the + // screen, but we are sending them to APZ which only knows about coordinates + // relative to the widget, so convert them accordingly. + const LayoutDeviceIntPoint anchor = + RoundedToInt(LayoutDevicePoint(aAnchorX, aAnchorY)) - + widget->WidgetToScreenOffset(); + + mozilla::layers::ScrollableLayerGuid guid(layersId, aPresShellId, aScrollId); + + return widget->StartAsyncAutoscroll( + ViewAs<ScreenPixel>( + anchor, PixelCastJustification::LayoutDeviceIsScreenForBounds), + guid); +} + +void CanonicalBrowsingContext::StopApzAutoscroll(nsViewID aScrollId, + uint32_t aPresShellId) { + nsCOMPtr<nsIWidget> widget; + mozilla::layers::LayersId layersId{0}; + + if (IsInProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> outer = GetDOMWindow(); + if (!outer) { + return; + } + + widget = widget::WidgetUtils::DOMWindowToWidget(outer); + if (widget) { + layersId = widget->GetRootLayerTreeId(); + } + } else { + RefPtr<BrowserParent> parent = GetBrowserParent(); + if (!parent) { + return; + } + + widget = parent->GetWidget(); + layersId = parent->GetLayersId(); + } + + if (!widget || !widget->AsyncPanZoomEnabled()) { + return; + } + + mozilla::layers::ScrollableLayerGuid guid(layersId, aPresShellId, aScrollId); + widget->StopAsyncAutoscroll(guid); +} + +already_AddRefed<nsISHEntry> +CanonicalBrowsingContext::GetMostRecentLoadingSessionHistoryEntry() { + if (mLoadingEntries.IsEmpty()) { + return nullptr; + } + + RefPtr<SessionHistoryEntry> entry = mLoadingEntries.LastElement().mEntry; + return entry.forget(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(CanonicalBrowsingContext) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(CanonicalBrowsingContext, + BrowsingContext) + tmp->mPermanentKey.setNull(); + if (tmp->mSessionHistory) { + tmp->mSessionHistory->SetBrowsingContext(nullptr); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSessionHistory, mContainerFeaturePolicy, + mCurrentBrowserParent, mWebProgress, + mSessionStoreSessionStorageUpdateTimer) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(CanonicalBrowsingContext, + BrowsingContext) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSessionHistory, mContainerFeaturePolicy, + mCurrentBrowserParent, mWebProgress, + mSessionStoreSessionStorageUpdateTimer) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(CanonicalBrowsingContext, + BrowsingContext) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPermanentKey) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(CanonicalBrowsingContext, BrowsingContext) +NS_IMPL_RELEASE_INHERITED(CanonicalBrowsingContext, BrowsingContext) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanonicalBrowsingContext) +NS_INTERFACE_MAP_END_INHERITING(BrowsingContext) + +} // namespace dom +} // namespace mozilla diff --git a/docshell/base/CanonicalBrowsingContext.h b/docshell/base/CanonicalBrowsingContext.h new file mode 100644 index 0000000000..9420c7e647 --- /dev/null +++ b/docshell/base/CanonicalBrowsingContext.h @@ -0,0 +1,601 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_CanonicalBrowsingContext_h +#define mozilla_dom_CanonicalBrowsingContext_h + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/MediaControlKeySource.h" +#include "mozilla/dom/BrowsingContextWebProgress.h" +#include "mozilla/dom/ProcessIsolation.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/SessionHistoryEntry.h" +#include "mozilla/dom/SessionStoreRestoreData.h" +#include "mozilla/dom/SessionStoreUtils.h" +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/RefPtr.h" +#include "mozilla/MozPromise.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsISecureBrowserUI.h" + +class nsIBrowserDOMWindow; +class nsISHistory; +class nsIWidget; +class nsIPrintSettings; +class nsSHistory; +class nsBrowserStatusFilter; +class nsSecureBrowserUI; +class CallerWillNotifyHistoryIndexAndLengthChanges; +class nsITimer; + +namespace mozilla { +enum class CallState; + +namespace embedding { +class PrintData; +} + +namespace net { +class DocumentLoadListener; +} + +namespace dom { + +class BrowserParent; +class BrowserBridgeParent; +class FeaturePolicy; +struct LoadURIOptions; +class MediaController; +struct LoadingSessionHistoryInfo; +class SSCacheCopy; +class WindowGlobalParent; +class SessionStoreFormData; +class SessionStoreScrollData; + +// CanonicalBrowsingContext is a BrowsingContext living in the parent +// process, with whatever extra data that a BrowsingContext in the +// parent needs. +class CanonicalBrowsingContext final : public BrowsingContext { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + CanonicalBrowsingContext, BrowsingContext) + + static already_AddRefed<CanonicalBrowsingContext> Get(uint64_t aId); + static CanonicalBrowsingContext* Cast(BrowsingContext* aContext); + static const CanonicalBrowsingContext* Cast(const BrowsingContext* aContext); + static already_AddRefed<CanonicalBrowsingContext> Cast( + already_AddRefed<BrowsingContext>&& aContext); + + bool IsOwnedByProcess(uint64_t aProcessId) const { + return mProcessId == aProcessId; + } + bool IsEmbeddedInProcess(uint64_t aProcessId) const { + return mEmbedderProcessId == aProcessId; + } + uint64_t OwnerProcessId() const { return mProcessId; } + uint64_t EmbedderProcessId() const { return mEmbedderProcessId; } + ContentParent* GetContentParent() const; + + void GetCurrentRemoteType(nsACString& aRemoteType, ErrorResult& aRv) const; + + void SetOwnerProcessId(uint64_t aProcessId); + + // The ID of the BrowsingContext which caused this BrowsingContext to be + // opened, or `0` if this is unknown. + // Only set for toplevel content BrowsingContexts, and may be from a different + // BrowsingContextGroup. + uint64_t GetCrossGroupOpenerId() const { return mCrossGroupOpenerId; } + void SetCrossGroupOpenerId(uint64_t aOpenerId); + void SetCrossGroupOpener(CanonicalBrowsingContext& aCrossGroupOpener, + ErrorResult& aRv); + + void GetWindowGlobals(nsTArray<RefPtr<WindowGlobalParent>>& aWindows); + + // The current active WindowGlobal. + WindowGlobalParent* GetCurrentWindowGlobal() const; + + // Same as the methods on `BrowsingContext`, but with the types already cast + // to the parent process type. + CanonicalBrowsingContext* GetParent() { + return Cast(BrowsingContext::GetParent()); + } + CanonicalBrowsingContext* Top() { return Cast(BrowsingContext::Top()); } + WindowGlobalParent* GetParentWindowContext(); + WindowGlobalParent* GetTopWindowContext(); + + already_AddRefed<nsIWidget> GetParentProcessWidgetContaining(); + already_AddRefed<nsIBrowserDOMWindow> GetBrowserDOMWindow(); + + // Same as `GetParentWindowContext`, but will also cross <browser> and + // content/chrome boundaries. + already_AddRefed<WindowGlobalParent> GetEmbedderWindowGlobal() const; + + already_AddRefed<CanonicalBrowsingContext> GetParentCrossChromeBoundary(); + + already_AddRefed<CanonicalBrowsingContext> TopCrossChromeBoundary(); + Nullable<WindowProxyHolder> GetTopChromeWindow(); + + nsISHistory* GetSessionHistory(); + SessionHistoryEntry* GetActiveSessionHistoryEntry(); + void SetActiveSessionHistoryEntry(SessionHistoryEntry* aEntry); + + UniquePtr<LoadingSessionHistoryInfo> CreateLoadingSessionHistoryEntryForLoad( + nsDocShellLoadState* aLoadState, SessionHistoryEntry* aExistingEntry, + nsIChannel* aChannel); + + UniquePtr<LoadingSessionHistoryInfo> ReplaceLoadingSessionHistoryEntryForLoad( + LoadingSessionHistoryInfo* aInfo, nsIChannel* aNewChannel); + + using PrintPromise = MozPromise</* unused */ bool, nsresult, false>; + MOZ_CAN_RUN_SCRIPT RefPtr<PrintPromise> Print(nsIPrintSettings*); + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PrintJS(nsIPrintSettings*, + ErrorResult&); + + // Call the given callback on all top-level descendant BrowsingContexts. + // Return Callstate::Stop from the callback to stop calling + // further children. + void CallOnAllTopDescendants( + const std::function<mozilla::CallState(CanonicalBrowsingContext*)>& + aCallback); + + void SessionHistoryCommit(uint64_t aLoadId, const nsID& aChangeID, + uint32_t aLoadType, bool aPersist, + bool aCloneEntryChildren, bool aChannelExpired, + uint32_t aCacheKey); + + // Calls the session history listeners' OnHistoryReload, storing the result in + // aCanReload. If aCanReload is set to true and we have an active or a loading + // entry then aLoadState will be initialized from that entry, and + // aReloadActiveEntry will be true if we have an active entry. If aCanReload + // is true and aLoadState and aReloadActiveEntry are not set then we should + // attempt to reload based on the current document in the docshell. + void NotifyOnHistoryReload( + bool aForceReload, bool& aCanReload, + Maybe<NotNull<RefPtr<nsDocShellLoadState>>>& aLoadState, + Maybe<bool>& aReloadActiveEntry); + + // See BrowsingContext::SetActiveSessionHistoryEntry. + void SetActiveSessionHistoryEntry(const Maybe<nsPoint>& aPreviousScrollPos, + SessionHistoryInfo* aInfo, + uint32_t aLoadType, + uint32_t aUpdatedCacheKey, + const nsID& aChangeID); + + void ReplaceActiveSessionHistoryEntry(SessionHistoryInfo* aInfo); + + void RemoveDynEntriesFromActiveSessionHistoryEntry(); + + void RemoveFromSessionHistory(const nsID& aChangeID); + + Maybe<int32_t> HistoryGo(int32_t aOffset, uint64_t aHistoryEpoch, + bool aRequireUserInteraction, bool aUserActivation, + Maybe<ContentParentId> aContentId); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Dispatches a wheel zoom change to the embedder element. + void DispatchWheelZoomChange(bool aIncrease); + + // This function is used to start the autoplay media which are delayed to + // start. If needed, it would also notify the content browsing context which + // are related with the canonical browsing content tree to start delayed + // autoplay media. + void NotifyStartDelayedAutoplayMedia(); + + // This function is used to mute or unmute all media within a tab. It would + // set the media mute property for the top level window and propagate it to + // other top level windows in other processes. + void NotifyMediaMutedChanged(bool aMuted, ErrorResult& aRv); + + // Return the number of unique site origins by iterating all given BCs, + // including their subtrees. + static uint32_t CountSiteOrigins( + GlobalObject& aGlobal, + const Sequence<mozilla::OwningNonNull<BrowsingContext>>& aRoots); + + // Return true if a private browsing session is active. + static bool IsPrivateBrowsingActive(); + + // This function would propogate the action to its all child browsing contexts + // in content processes. + void UpdateMediaControlAction(const MediaControlAction& aAction); + + // Triggers a load in the process + using BrowsingContext::LoadURI; + void FixupAndLoadURIString(const nsAString& aURI, + const LoadURIOptions& aOptions, + ErrorResult& aError); + void LoadURI(nsIURI* aURI, const LoadURIOptions& aOptions, + ErrorResult& aError); + + void GoBack(const Optional<int32_t>& aCancelContentJSEpoch, + bool aRequireUserInteraction, bool aUserActivation); + void GoForward(const Optional<int32_t>& aCancelContentJSEpoch, + bool aRequireUserInteraction, bool aUserActivation); + void GoToIndex(int32_t aIndex, const Optional<int32_t>& aCancelContentJSEpoch, + bool aUserActivation); + void Reload(uint32_t aReloadFlags); + void Stop(uint32_t aStopFlags); + + // Get the publicly exposed current URI loaded in this BrowsingContext. + already_AddRefed<nsIURI> GetCurrentURI() const; + void SetCurrentRemoteURI(nsIURI* aCurrentRemoteURI); + + BrowserParent* GetBrowserParent() const; + void SetCurrentBrowserParent(BrowserParent* aBrowserParent); + + // Internal method to change which process a BrowsingContext is being loaded + // in. The returned promise will resolve when the process switch is completed. + // + // A NOT_REMOTE_TYPE aRemoteType argument will perform a process switch into + // the parent process, and the method will resolve with a null BrowserParent. + using RemotenessPromise = MozPromise<RefPtr<BrowserParent>, nsresult, false>; + RefPtr<RemotenessPromise> ChangeRemoteness( + const NavigationIsolationOptions& aOptions, uint64_t aPendingSwitchId); + + // Return a media controller from the top-level browsing context that can + // control all media belonging to this browsing context tree. Return nullptr + // if the top-level browsing context has been discarded. + MediaController* GetMediaController(); + bool HasCreatedMediaController() const; + + // Attempts to start loading the given load state in this BrowsingContext, + // without requiring any communication from a docshell. This will handle + // computing the right process to load in, and organising handoff to + // the right docshell when we get a response. + bool LoadInParent(nsDocShellLoadState* aLoadState, bool aSetNavigating); + + // Attempts to start loading the given load state in this BrowsingContext, + // in parallel with a DocumentChannelChild being created in the docshell. + // Requires the DocumentChannel to connect with this load for it to + // complete successfully. + bool AttemptSpeculativeLoadInParent(nsDocShellLoadState* aLoadState); + + // Get or create a secure browser UI for this BrowsingContext + nsISecureBrowserUI* GetSecureBrowserUI(); + + BrowsingContextWebProgress* GetWebProgress() { return mWebProgress; } + + // Called when the current URI changes (from an + // nsIWebProgressListener::OnLocationChange event, so that we + // can update our security UI for the new location, or when the + // mixed content/https-only state for our current window is changed. + void UpdateSecurityState(); + + void MaybeAddAsProgressListener(nsIWebProgress* aWebProgress); + + // Called when a navigation forces us to recreate our browsing + // context (for example, when switching in or out of the parent + // process). + // aNewContext is the newly created BrowsingContext that is replacing + // us. + void ReplacedBy(CanonicalBrowsingContext* aNewContext, + const NavigationIsolationOptions& aRemotenessOptions); + + bool HasHistoryEntry(nsISHEntry* aEntry); + bool HasLoadingHistoryEntry(nsISHEntry* aEntry) { + for (const LoadingSessionHistoryEntry& loading : mLoadingEntries) { + if (loading.mEntry == aEntry) { + return true; + } + } + return false; + } + + void SwapHistoryEntries(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry); + + void AddLoadingSessionHistoryEntry(uint64_t aLoadId, + SessionHistoryEntry* aEntry); + + void GetLoadingSessionHistoryInfoFromParent( + Maybe<LoadingSessionHistoryInfo>& aLoadingInfo); + + void HistoryCommitIndexAndLength(); + + void SynchronizeLayoutHistoryState(); + + void ResetScalingZoom(); + + void SetContainerFeaturePolicy(FeaturePolicy* aContainerFeaturePolicy); + FeaturePolicy* GetContainerFeaturePolicy() const { + return mContainerFeaturePolicy; + } + + void SetRestoreData(SessionStoreRestoreData* aData, ErrorResult& aError); + void ClearRestoreState(); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void RequestRestoreTabContent( + WindowGlobalParent* aWindow); + already_AddRefed<Promise> GetRestorePromise(); + + nsresult WriteSessionStorageToSessionStore( + const nsTArray<SSCacheCopy>& aSesssionStorage, uint32_t aEpoch); + + void UpdateSessionStoreSessionStorage(const std::function<void()>& aDone); + + static void UpdateSessionStoreForStorage(uint64_t aBrowsingContextId); + + // Called when a BrowserParent for this BrowsingContext has been fully + // destroyed (i.e. `ActorDestroy` was called). + void BrowserParentDestroyed(BrowserParent* aBrowserParent, + bool aAbnormalShutdown); + + void StartUnloadingHost(uint64_t aChildID); + void ClearUnloadingHost(uint64_t aChildID); + + bool AllowedInBFCache(const Maybe<uint64_t>& aChannelId, nsIURI* aNewURI); + + // Methods for getting and setting the active state for top level + // browsing contexts, for the process priority manager. + bool IsPriorityActive() const { + MOZ_RELEASE_ASSERT(IsTop()); + return mPriorityActive; + } + void SetPriorityActive(bool aIsActive) { + MOZ_RELEASE_ASSERT(IsTop()); + mPriorityActive = aIsActive; + } + + void SetTouchEventsOverride(dom::TouchEventsOverride, ErrorResult& aRv); + void SetTargetTopLevelLinkClicksToBlank(bool aTargetTopLevelLinkClicksToBlank, + ErrorResult& aRv); + + bool IsReplaced() const { return mIsReplaced; } + + const JS::Heap<JS::Value>& PermanentKey() { return mPermanentKey; } + void ClearPermanentKey() { mPermanentKey.setNull(); } + void MaybeSetPermanentKey(Element* aEmbedder); + + // When request for page awake, it would increase a count that is used to + // prevent whole browsing context tree from being suspended. The request can + // be called multiple times. When calling the revoke, it would decrease the + // count and once the count reaches to zero, the browsing context tree could + // be suspended when the tree is inactive. + void AddPageAwakeRequest(); + void RemovePageAwakeRequest(); + + void CloneDocumentTreeInto(CanonicalBrowsingContext* aSource, + const nsACString& aRemoteType, + embedding::PrintData&& aPrintData); + + // Returns a Promise which resolves when cloning documents for printing + // finished if this browsing context is cloning document tree. + RefPtr<GenericNonExclusivePromise> GetClonePromise() const { + return mClonePromise; + } + + bool StartApzAutoscroll(float aAnchorX, float aAnchorY, nsViewID aScrollId, + uint32_t aPresShellId); + void StopApzAutoscroll(nsViewID aScrollId, uint32_t aPresShellId); + + void AddFinalDiscardListener(std::function<void(uint64_t)>&& aListener); + + bool ForceAppWindowActive() const { return mForceAppWindowActive; } + void SetForceAppWindowActive(bool, ErrorResult&); + void RecomputeAppWindowVisibility(); + + already_AddRefed<nsISHEntry> GetMostRecentLoadingSessionHistoryEntry(); + + protected: + // Called when the browsing context is being discarded. + void CanonicalDiscard(); + + // Called when the browsing context is being attached. + void CanonicalAttach(); + + // Called when the browsing context private mode is changed after + // being attached, but before being discarded. + void AdjustPrivateBrowsingCount(bool aPrivateBrowsing); + + using Type = BrowsingContext::Type; + CanonicalBrowsingContext(WindowContext* aParentWindow, + BrowsingContextGroup* aGroup, + uint64_t aBrowsingContextId, + uint64_t aOwnerProcessId, + uint64_t aEmbedderProcessId, Type aType, + FieldValues&& aInit); + + private: + friend class BrowsingContext; + + virtual ~CanonicalBrowsingContext(); + + class PendingRemotenessChange { + public: + NS_INLINE_DECL_REFCOUNTING(PendingRemotenessChange) + + PendingRemotenessChange(CanonicalBrowsingContext* aTarget, + RemotenessPromise::Private* aPromise, + uint64_t aPendingSwitchId, + const NavigationIsolationOptions& aOptions); + + void Cancel(nsresult aRv); + + private: + friend class CanonicalBrowsingContext; + + ~PendingRemotenessChange(); + void ProcessLaunched(); + void ProcessReady(); + void MaybeFinish(); + void Clear(); + + nsresult FinishTopContent(); + nsresult FinishSubframe(); + + RefPtr<CanonicalBrowsingContext> mTarget; + RefPtr<RemotenessPromise::Private> mPromise; + RefPtr<ContentParent> mContentParent; + RefPtr<BrowsingContextGroup> mSpecificGroup; + + bool mProcessReady = false; + bool mWaitingForPrepareToChange = false; + + uint64_t mPendingSwitchId; + NavigationIsolationOptions mOptions; + }; + + struct RestoreState { + NS_INLINE_DECL_REFCOUNTING(RestoreState) + + void ClearData() { mData = nullptr; } + void Resolve(); + + RefPtr<SessionStoreRestoreData> mData; + RefPtr<Promise> mPromise; + uint32_t mRequests = 0; + uint32_t mResolves = 0; + + private: + ~RestoreState() = default; + }; + + friend class net::DocumentLoadListener; + // Called when a DocumentLoadListener is created to start a load for + // this browsing context. Returns false if a higher priority load is + // already in-progress and the new one has been rejected. + bool StartDocumentLoad(net::DocumentLoadListener* aLoad); + // Called once DocumentLoadListener completes handling a load, and it + // is either complete, or handed off to the final channel to deliver + // data to the destination docshell. + // If aContinueNavigating it set, the reference to the DocumentLoadListener + // will be cleared to prevent it being cancelled, however the current load ID + // will be preserved until `EndDocumentLoad` is called again. + void EndDocumentLoad(bool aContinueNavigating); + + bool SupportsLoadingInParent(nsDocShellLoadState* aLoadState, + uint64_t* aOuterWindowId); + + void HistoryCommitIndexAndLength( + const nsID& aChangeID, + const CallerWillNotifyHistoryIndexAndLengthChanges& aProofOfCaller); + + struct UnloadingHost { + uint64_t mChildID; + nsTArray<std::function<void()>> mCallbacks; + }; + nsTArray<UnloadingHost>::iterator FindUnloadingHost(uint64_t aChildID); + + // Called when we want to show the subframe crashed UI as our previous browser + // has become unloaded for one reason or another. + void ShowSubframeCrashedUI(BrowserBridgeParent* aBridge); + + void MaybeScheduleSessionStoreUpdate(); + + void CancelSessionStoreUpdate(); + + void AddPendingDiscard(); + + void RemovePendingDiscard(); + + bool ShouldAddEntryForRefresh(const SessionHistoryEntry* aEntry) { + return ShouldAddEntryForRefresh(aEntry->Info().GetURI(), + aEntry->Info().HasPostData()); + } + bool ShouldAddEntryForRefresh(nsIURI* aNewURI, bool aHasPostData) { + nsCOMPtr<nsIURI> currentURI = GetCurrentURI(); + return BrowsingContext::ShouldAddEntryForRefresh(currentURI, aNewURI, + aHasPostData); + } + + already_AddRefed<nsDocShellLoadState> CreateLoadInfo( + SessionHistoryEntry* aEntry); + + // XXX(farre): Store a ContentParent pointer here rather than mProcessId? + // Indicates which process owns the docshell. + uint64_t mProcessId; + + // Indicates which process owns the embedder element. + uint64_t mEmbedderProcessId; + + uint64_t mCrossGroupOpenerId = 0; + + // This function will make the top window context reset its + // "SHEntryHasUserInteraction" cache that prevents documents from repeatedly + // setting user interaction on SH entries. Should be called anytime SH + // entries are added or replaced. + void ResetSHEntryHasUserInteractionCache(); + + RefPtr<BrowserParent> mCurrentBrowserParent; + + nsTArray<UnloadingHost> mUnloadingHosts; + + // The current URI loaded in this BrowsingContext. This value is only set for + // BrowsingContexts loaded in content processes. + nsCOMPtr<nsIURI> mCurrentRemoteURI; + + // The current remoteness change which is in a pending state. + RefPtr<PendingRemotenessChange> mPendingRemotenessChange; + + RefPtr<nsSHistory> mSessionHistory; + + // Tab media controller is used to control all media existing in the same + // browsing context tree, so it would only exist in the top level browsing + // context. + RefPtr<MediaController> mTabMediaController; + + RefPtr<net::DocumentLoadListener> mCurrentLoad; + + struct LoadingSessionHistoryEntry { + uint64_t mLoadId = 0; + RefPtr<SessionHistoryEntry> mEntry; + }; + nsTArray<LoadingSessionHistoryEntry> mLoadingEntries; + RefPtr<SessionHistoryEntry> mActiveEntry; + + RefPtr<nsSecureBrowserUI> mSecureBrowserUI; + RefPtr<BrowsingContextWebProgress> mWebProgress; + + nsCOMPtr<nsIWebProgressListener> mDocShellProgressBridge; + RefPtr<nsBrowserStatusFilter> mStatusFilter; + + RefPtr<FeaturePolicy> mContainerFeaturePolicy; + + friend class BrowserSessionStore; + WeakPtr<SessionStoreFormData>& GetSessionStoreFormDataRef() { + return mFormdata; + } + WeakPtr<SessionStoreScrollData>& GetSessionStoreScrollDataRef() { + return mScroll; + } + + WeakPtr<SessionStoreFormData> mFormdata; + WeakPtr<SessionStoreScrollData> mScroll; + + RefPtr<RestoreState> mRestoreState; + + nsCOMPtr<nsITimer> mSessionStoreSessionStorageUpdateTimer; + + // If this is a top level context, this is true if our browser ID is marked as + // active in the process priority manager. + bool mPriorityActive = false; + + // See CanonicalBrowsingContext.forceAppWindowActive. + bool mForceAppWindowActive = false; + + bool mIsReplaced = false; + + // A Promise created when cloning documents for printing. + RefPtr<GenericNonExclusivePromise> mClonePromise; + + JS::Heap<JS::Value> mPermanentKey; + + uint32_t mPendingDiscards = 0; + + bool mFullyDiscarded = false; + + nsTArray<std::function<void(uint64_t)>> mFullyDiscardedListeners; +}; + +} // namespace dom +} // namespace mozilla + +#endif // !defined(mozilla_dom_CanonicalBrowsingContext_h) diff --git a/docshell/base/ChildProcessChannelListener.cpp b/docshell/base/ChildProcessChannelListener.cpp new file mode 100644 index 0000000000..628d75abb1 --- /dev/null +++ b/docshell/base/ChildProcessChannelListener.cpp @@ -0,0 +1,61 @@ +/* -*- 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/. */ + +#include "mozilla/dom/ChildProcessChannelListener.h" + +#include "mozilla/ipc/Endpoint.h" +#include "nsDocShellLoadState.h" + +namespace mozilla { +namespace dom { + +static StaticRefPtr<ChildProcessChannelListener> sCPCLSingleton; + +void ChildProcessChannelListener::RegisterCallback(uint64_t aIdentifier, + Callback&& aCallback) { + if (auto args = mChannelArgs.Extract(aIdentifier)) { + nsresult rv = + aCallback(args->mLoadState, std::move(args->mStreamFilterEndpoints), + args->mTiming); + args->mResolver(rv); + } else { + mCallbacks.InsertOrUpdate(aIdentifier, std::move(aCallback)); + } +} + +void ChildProcessChannelListener::OnChannelReady( + nsDocShellLoadState* aLoadState, uint64_t aIdentifier, + nsTArray<Endpoint>&& aStreamFilterEndpoints, nsDOMNavigationTiming* aTiming, + Resolver&& aResolver) { + if (auto callback = mCallbacks.Extract(aIdentifier)) { + nsresult rv = + (*callback)(aLoadState, std::move(aStreamFilterEndpoints), aTiming); + aResolver(rv); + } else { + mChannelArgs.InsertOrUpdate( + aIdentifier, CallbackArgs{aLoadState, std::move(aStreamFilterEndpoints), + aTiming, std::move(aResolver)}); + } +} + +ChildProcessChannelListener::~ChildProcessChannelListener() { + for (const auto& args : mChannelArgs.Values()) { + args.mResolver(NS_ERROR_FAILURE); + } +} + +already_AddRefed<ChildProcessChannelListener> +ChildProcessChannelListener::GetSingleton() { + if (!sCPCLSingleton) { + sCPCLSingleton = new ChildProcessChannelListener(); + ClearOnShutdown(&sCPCLSingleton); + } + RefPtr<ChildProcessChannelListener> cpcl = sCPCLSingleton; + return cpcl.forget(); +} + +} // namespace dom +} // namespace mozilla diff --git a/docshell/base/ChildProcessChannelListener.h b/docshell/base/ChildProcessChannelListener.h new file mode 100644 index 0000000000..c00c2ff5a7 --- /dev/null +++ b/docshell/base/ChildProcessChannelListener.h @@ -0,0 +1,54 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ChildProcessChannelListener_h +#define mozilla_dom_ChildProcessChannelListener_h + +#include <functional> + +#include "mozilla/extensions/StreamFilterParent.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "nsDOMNavigationTiming.h" +#include "nsTHashMap.h" +#include "nsIChannel.h" +#include "mozilla/ipc/BackgroundUtils.h" + +namespace mozilla::dom { + +class ChildProcessChannelListener final { + NS_INLINE_DECL_REFCOUNTING(ChildProcessChannelListener) + + using Endpoint = mozilla::ipc::Endpoint<extensions::PStreamFilterParent>; + using Resolver = std::function<void(const nsresult&)>; + using Callback = std::function<nsresult( + nsDocShellLoadState*, nsTArray<Endpoint>&&, nsDOMNavigationTiming*)>; + + void RegisterCallback(uint64_t aIdentifier, Callback&& aCallback); + + void OnChannelReady(nsDocShellLoadState* aLoadState, uint64_t aIdentifier, + nsTArray<Endpoint>&& aStreamFilterEndpoints, + nsDOMNavigationTiming* aTiming, Resolver&& aResolver); + + static already_AddRefed<ChildProcessChannelListener> GetSingleton(); + + private: + ChildProcessChannelListener() = default; + ~ChildProcessChannelListener(); + struct CallbackArgs { + RefPtr<nsDocShellLoadState> mLoadState; + nsTArray<Endpoint> mStreamFilterEndpoints; + RefPtr<nsDOMNavigationTiming> mTiming; + Resolver mResolver; + }; + + // TODO Backtrack. + nsTHashMap<nsUint64HashKey, Callback> mCallbacks; + nsTHashMap<nsUint64HashKey, CallbackArgs> mChannelArgs; +}; + +} // namespace mozilla::dom + +#endif // !defined(mozilla_dom_ChildProcessChannelListener_h) diff --git a/docshell/base/IHistory.h b/docshell/base/IHistory.h new file mode 100644 index 0000000000..594ebdb3bb --- /dev/null +++ b/docshell/base/IHistory.h @@ -0,0 +1,169 @@ +/* -*- 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/. */ + +#ifndef mozilla_IHistory_h_ +#define mozilla_IHistory_h_ + +#include "nsISupports.h" +#include "nsURIHashKey.h" +#include "nsTHashSet.h" +#include "nsTObserverArray.h" + +class nsIURI; +class nsIWidget; + +namespace mozilla { + +namespace dom { +class ContentParent; +class Document; +class Link; +} // namespace dom + +// 0057c9d3-b98e-4933-bdc5-0275d06705e1 +#define IHISTORY_IID \ + { \ + 0x0057c9d3, 0xb98e, 0x4933, { \ + 0xbd, 0xc5, 0x02, 0x75, 0xd0, 0x67, 0x05, 0xe1 \ + } \ + } + +class IHistory : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(IHISTORY_IID) + + using ContentParentSet = nsTHashSet<RefPtr<dom::ContentParent>>; + + /** + * Registers the Link for notifications about the visited-ness of aURI. + * Consumers should assume that the URI is unvisited after calling this, and + * they will be notified if that state (unvisited) changes by having + * VisitedQueryFinished called on themselves. Note that it may call + * synchronously if the answer is already known. + * + * @note VisitedQueryFinished must not call RegisterVisitedCallback or + * UnregisterVisitedCallback. + * + * @pre aURI must not be null. + * @pre aLink may be null only in the parent (chrome) process. + * + * @param aURI + * The URI to check. + * @param aLink + * The link to update whenever the history status changes. The + * implementation will only hold onto a raw pointer, so if this + * object should be destroyed, be sure to call + * UnregisterVistedCallback first. + */ + virtual void RegisterVisitedCallback(nsIURI* aURI, dom::Link* aLink) = 0; + + /** + * Schedules a single visited query from a given child process. + * + * @param aURI the URI to query. + * @param ContentParent the process interested in knowing about the visited + * state of this URI. + */ + virtual void ScheduleVisitedQuery(nsIURI* aURI, dom::ContentParent*) = 0; + + /** + * Unregisters a previously registered Link object. This must be called + * before destroying the registered object, and asserts when misused. + * + * @pre aURI must not be null. + * @pre aLink must not be null. + * + * @param aURI + * The URI that aLink was registered for. + * @param aLink + * The link object to unregister for aURI. + */ + virtual void UnregisterVisitedCallback(nsIURI* aURI, dom::Link* aLink) = 0; + + enum class VisitedStatus : uint8_t { + Unknown, + Visited, + Unvisited, + }; + + /** + * Notifies about the visited status of a given URI. The visited status cannot + * be unknown, otherwise there's no point in notifying of anything. + * + * @param ContentParentSet a set of content processes that are interested on + * this change. If null, it is broadcasted to all + * child processes. + */ + virtual void NotifyVisited(nsIURI*, VisitedStatus, + const ContentParentSet* = nullptr) = 0; + + enum VisitFlags { + /** + * Indicates whether the URI was loaded in a top-level window. + */ + TOP_LEVEL = 1 << 0, + /** + * Indicates whether the URI is the target of a permanent redirect. + */ + REDIRECT_PERMANENT = 1 << 1, + /** + * Indicates whether the URI is the target of a temporary redirect. + */ + REDIRECT_TEMPORARY = 1 << 2, + /** + * Indicates the URI will redirect (Response code 3xx). + */ + REDIRECT_SOURCE = 1 << 3, + /** + * Indicates the URI caused an error that is unlikely fixable by a + * retry, like a not found or unfetchable page. + */ + UNRECOVERABLE_ERROR = 1 << 4, + /** + * If REDIRECT_SOURCE is set, this indicates that the redirect is permanent. + * Note this differs from REDIRECT_PERMANENT because that one refers to how + * we reached the URI, while this is used when the URI itself redirects. + */ + REDIRECT_SOURCE_PERMANENT = 1 << 5 + }; + + /** + * Adds a history visit for the URI. + * + * @pre aURI must not be null. + * + * @param aWidget + * The widget for the DocShell. + * @param aURI + * The URI of the page being visited. + * @param aLastVisitedURI + * The URI of the last visit in the chain. + * @param aFlags + * The VisitFlags describing this visit. + * @param aBrowserId + * The id of browser used for this visit. + */ + NS_IMETHOD VisitURI(nsIWidget* aWidget, nsIURI* aURI, nsIURI* aLastVisitedURI, + uint32_t aFlags, uint64_t aBrowserId) = 0; + + /** + * Set the title of the URI. + * + * @pre aURI must not be null. + * + * @param aURI + * The URI to set the title for. + * @param aTitle + * The title string. + */ + NS_IMETHOD SetURITitle(nsIURI* aURI, const nsAString& aTitle) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(IHistory, IHISTORY_IID) + +} // namespace mozilla + +#endif // mozilla_IHistory_h_ diff --git a/docshell/base/LoadContext.cpp b/docshell/base/LoadContext.cpp new file mode 100644 index 0000000000..2e501be221 --- /dev/null +++ b/docshell/base/LoadContext.cpp @@ -0,0 +1,236 @@ +/* -*- 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/. */ + +#include "mozilla/Assertions.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/LoadContext.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ScriptSettings.h" // for AutoJSAPI +#include "mozilla/dom/BrowsingContext.h" +#include "nsContentUtils.h" +#include "xpcpublic.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(LoadContext, nsILoadContext, nsIInterfaceRequestor) + +LoadContext::LoadContext(const IPC::SerializedLoadContext& aToCopy, + dom::Element* aTopFrameElement, + OriginAttributes& aAttrs) + : mTopFrameElement(do_GetWeakReference(aTopFrameElement)), + mIsContent(aToCopy.mIsContent), + mUseRemoteTabs(aToCopy.mUseRemoteTabs), + mUseRemoteSubframes(aToCopy.mUseRemoteSubframes), + mUseTrackingProtection(aToCopy.mUseTrackingProtection), +#ifdef DEBUG + mIsNotNull(aToCopy.mIsNotNull), +#endif + mOriginAttributes(aAttrs) { +} + +LoadContext::LoadContext(OriginAttributes& aAttrs) + : mTopFrameElement(nullptr), + mIsContent(false), + mUseRemoteTabs(false), + mUseRemoteSubframes(false), + mUseTrackingProtection(false), +#ifdef DEBUG + mIsNotNull(true), +#endif + mOriginAttributes(aAttrs) { +} + +LoadContext::LoadContext(nsIPrincipal* aPrincipal, + nsILoadContext* aOptionalBase) + : mTopFrameElement(nullptr), + mIsContent(true), + mUseRemoteTabs(false), + mUseRemoteSubframes(false), + mUseTrackingProtection(false), +#ifdef DEBUG + mIsNotNull(true), +#endif + mOriginAttributes(aPrincipal->OriginAttributesRef()) { + if (!aOptionalBase) { + return; + } + + MOZ_ALWAYS_SUCCEEDS(aOptionalBase->GetIsContent(&mIsContent)); + MOZ_ALWAYS_SUCCEEDS(aOptionalBase->GetUseRemoteTabs(&mUseRemoteTabs)); + MOZ_ALWAYS_SUCCEEDS( + aOptionalBase->GetUseRemoteSubframes(&mUseRemoteSubframes)); + MOZ_ALWAYS_SUCCEEDS( + aOptionalBase->GetUseTrackingProtection(&mUseTrackingProtection)); +} + +LoadContext::~LoadContext() = default; + +//----------------------------------------------------------------------------- +// LoadContext::nsILoadContext +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +LoadContext::GetAssociatedWindow(mozIDOMWindowProxy**) { + MOZ_ASSERT(mIsNotNull); + + // can't support this in the parent process + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +LoadContext::GetTopWindow(mozIDOMWindowProxy**) { + MOZ_ASSERT(mIsNotNull); + + // can't support this in the parent process + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +LoadContext::GetTopFrameElement(dom::Element** aElement) { + nsCOMPtr<dom::Element> element = do_QueryReferent(mTopFrameElement); + element.forget(aElement); + return NS_OK; +} + +NS_IMETHODIMP +LoadContext::GetIsContent(bool* aIsContent) { + MOZ_ASSERT(mIsNotNull); + + NS_ENSURE_ARG_POINTER(aIsContent); + + *aIsContent = mIsContent; + return NS_OK; +} + +NS_IMETHODIMP +LoadContext::GetUsePrivateBrowsing(bool* aUsePrivateBrowsing) { + MOZ_ASSERT(mIsNotNull); + + NS_ENSURE_ARG_POINTER(aUsePrivateBrowsing); + + *aUsePrivateBrowsing = mOriginAttributes.mPrivateBrowsingId > 0; + return NS_OK; +} + +NS_IMETHODIMP +LoadContext::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) { + MOZ_ASSERT(mIsNotNull); + + // We shouldn't need this on parent... + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +LoadContext::SetPrivateBrowsing(bool aUsePrivateBrowsing) { + MOZ_ASSERT(mIsNotNull); + + // We shouldn't need this on parent... + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +LoadContext::GetUseRemoteTabs(bool* aUseRemoteTabs) { + MOZ_ASSERT(mIsNotNull); + + NS_ENSURE_ARG_POINTER(aUseRemoteTabs); + + *aUseRemoteTabs = mUseRemoteTabs; + return NS_OK; +} + +NS_IMETHODIMP +LoadContext::SetRemoteTabs(bool aUseRemoteTabs) { + MOZ_ASSERT(mIsNotNull); + + // We shouldn't need this on parent... + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +LoadContext::GetUseRemoteSubframes(bool* aUseRemoteSubframes) { + MOZ_ASSERT(mIsNotNull); + + NS_ENSURE_ARG_POINTER(aUseRemoteSubframes); + + *aUseRemoteSubframes = mUseRemoteSubframes; + return NS_OK; +} + +NS_IMETHODIMP +LoadContext::SetRemoteSubframes(bool aUseRemoteSubframes) { + MOZ_ASSERT(mIsNotNull); + + // We shouldn't need this on parent... + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +LoadContext::GetScriptableOriginAttributes( + JSContext* aCx, JS::MutableHandle<JS::Value> aAttrs) { + bool ok = ToJSValue(aCx, mOriginAttributes, aAttrs); + NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE); + return NS_OK; +} + +NS_IMETHODIMP_(void) +LoadContext::GetOriginAttributes(mozilla::OriginAttributes& aAttrs) { + aAttrs = mOriginAttributes; +} + +NS_IMETHODIMP +LoadContext::GetUseTrackingProtection(bool* aUseTrackingProtection) { + MOZ_ASSERT(mIsNotNull); + + NS_ENSURE_ARG_POINTER(aUseTrackingProtection); + + *aUseTrackingProtection = mUseTrackingProtection; + return NS_OK; +} + +NS_IMETHODIMP +LoadContext::SetUseTrackingProtection(bool aUseTrackingProtection) { + MOZ_ASSERT_UNREACHABLE("Should only be set through nsDocShell"); + + return NS_ERROR_UNEXPECTED; +} + +//----------------------------------------------------------------------------- +// LoadContext::nsIInterfaceRequestor +//----------------------------------------------------------------------------- +NS_IMETHODIMP +LoadContext::GetInterface(const nsIID& aIID, void** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = nullptr; + + if (aIID.Equals(NS_GET_IID(nsILoadContext))) { + *aResult = static_cast<nsILoadContext*>(this); + NS_ADDREF_THIS(); + return NS_OK; + } + + return NS_NOINTERFACE; +} + +static already_AddRefed<nsILoadContext> CreateInstance(bool aPrivate) { + OriginAttributes oa; + oa.mPrivateBrowsingId = aPrivate ? 1 : 0; + + nsCOMPtr<nsILoadContext> lc = new LoadContext(oa); + + return lc.forget(); +} + +already_AddRefed<nsILoadContext> CreateLoadContext() { + return CreateInstance(false); +} + +already_AddRefed<nsILoadContext> CreatePrivateLoadContext() { + return CreateInstance(true); +} + +} // namespace mozilla diff --git a/docshell/base/LoadContext.h b/docshell/base/LoadContext.h new file mode 100644 index 0000000000..5cb71ff347 --- /dev/null +++ b/docshell/base/LoadContext.h @@ -0,0 +1,68 @@ +/* -*- 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/. */ + +#ifndef LoadContext_h +#define LoadContext_h + +#include "SerializedLoadContext.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasePrincipal.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIInterfaceRequestor.h" +#include "nsILoadContext.h" + +namespace mozilla::dom { +class Element; +} + +namespace mozilla { + +/** + * Class that provides nsILoadContext info in Parent process. Typically copied + * from Child via SerializedLoadContext. + * + * Note: this is not the "normal" or "original" nsILoadContext. That is + * typically provided by BrowsingContext. This is only used when the original + * docshell is in a different process and we need to copy certain values from + * it. + */ + +class LoadContext final : public nsILoadContext, public nsIInterfaceRequestor { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSILOADCONTEXT + NS_DECL_NSIINTERFACEREQUESTOR + + LoadContext(const IPC::SerializedLoadContext& aToCopy, + dom::Element* aTopFrameElement, OriginAttributes& aAttrs); + + // Constructor taking reserved origin attributes. + explicit LoadContext(OriginAttributes& aAttrs); + + // Constructor for creating a LoadContext with a given browser flag. + explicit LoadContext(nsIPrincipal* aPrincipal, + nsILoadContext* aOptionalBase = nullptr); + + private: + ~LoadContext(); + + nsWeakPtr mTopFrameElement; + bool mIsContent; + bool mUseRemoteTabs; + bool mUseRemoteSubframes; + bool mUseTrackingProtection; +#ifdef DEBUG + bool mIsNotNull; +#endif + OriginAttributes mOriginAttributes; +}; + +already_AddRefed<nsILoadContext> CreateLoadContext(); +already_AddRefed<nsILoadContext> CreatePrivateLoadContext(); + +} // namespace mozilla + +#endif // LoadContext_h diff --git a/docshell/base/SerializedLoadContext.cpp b/docshell/base/SerializedLoadContext.cpp new file mode 100644 index 0000000000..cb598b43fe --- /dev/null +++ b/docshell/base/SerializedLoadContext.cpp @@ -0,0 +1,87 @@ +/* -*- 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/. */ + +#include "SerializedLoadContext.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsILoadContext.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsIWebSocketChannel.h" + +namespace IPC { + +SerializedLoadContext::SerializedLoadContext(nsILoadContext* aLoadContext) + : mIsContent(false), + mUseRemoteTabs(false), + mUseRemoteSubframes(false), + mUseTrackingProtection(false) { + Init(aLoadContext); +} + +SerializedLoadContext::SerializedLoadContext(nsIChannel* aChannel) + : mIsContent(false), + mUseRemoteTabs(false), + mUseRemoteSubframes(false), + mUseTrackingProtection(false) { + if (!aChannel) { + Init(nullptr); + return; + } + + nsCOMPtr<nsILoadContext> loadContext; + NS_QueryNotificationCallbacks(aChannel, loadContext); + Init(loadContext); + + if (!loadContext) { + // Attempt to retrieve the private bit from the channel if it has been + // overriden. + bool isPrivate = false; + bool isOverriden = false; + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel); + if (pbChannel && + NS_SUCCEEDED( + pbChannel->IsPrivateModeOverriden(&isPrivate, &isOverriden)) && + isOverriden) { + mIsPrivateBitValid = true; + } + mOriginAttributes.SyncAttributesWithPrivateBrowsing(isPrivate); + } +} + +SerializedLoadContext::SerializedLoadContext(nsIWebSocketChannel* aChannel) + : mIsContent(false), + mUseRemoteTabs(false), + mUseRemoteSubframes(false), + mUseTrackingProtection(false) { + nsCOMPtr<nsILoadContext> loadContext; + if (aChannel) { + NS_QueryNotificationCallbacks(aChannel, loadContext); + } + Init(loadContext); +} + +void SerializedLoadContext::Init(nsILoadContext* aLoadContext) { + if (aLoadContext) { + mIsNotNull = true; + mIsPrivateBitValid = true; + aLoadContext->GetIsContent(&mIsContent); + aLoadContext->GetUseRemoteTabs(&mUseRemoteTabs); + aLoadContext->GetUseRemoteSubframes(&mUseRemoteSubframes); + aLoadContext->GetUseTrackingProtection(&mUseTrackingProtection); + aLoadContext->GetOriginAttributes(mOriginAttributes); + } else { + mIsNotNull = false; + mIsPrivateBitValid = false; + // none of below values really matter when mIsNotNull == false: + // we won't be GetInterfaced to nsILoadContext + mIsContent = true; + mUseRemoteTabs = false; + mUseRemoteSubframes = false; + mUseTrackingProtection = false; + } +} + +} // namespace IPC diff --git a/docshell/base/SerializedLoadContext.h b/docshell/base/SerializedLoadContext.h new file mode 100644 index 0000000000..d47ad08003 --- /dev/null +++ b/docshell/base/SerializedLoadContext.h @@ -0,0 +1,96 @@ +/* -*- 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/. */ + +#ifndef SerializedLoadContext_h +#define SerializedLoadContext_h + +#include "base/basictypes.h" +#include "ipc/IPCMessageUtils.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "mozilla/BasePrincipal.h" + +class nsILoadContext; + +/* + * This file contains the IPC::SerializedLoadContext class, which is used to + * copy data across IPDL from Child process contexts so it is available in the + * Parent. + */ + +class nsIChannel; +class nsIWebSocketChannel; + +namespace IPC { + +class SerializedLoadContext { + public: + SerializedLoadContext() + : mIsNotNull(false), + mIsPrivateBitValid(false), + mIsContent(false), + mUseRemoteTabs(false), + mUseRemoteSubframes(false), + mUseTrackingProtection(false) { + Init(nullptr); + } + + explicit SerializedLoadContext(nsILoadContext* aLoadContext); + explicit SerializedLoadContext(nsIChannel* aChannel); + explicit SerializedLoadContext(nsIWebSocketChannel* aChannel); + + void Init(nsILoadContext* aLoadContext); + + bool IsNotNull() const { return mIsNotNull; } + bool IsPrivateBitValid() const { return mIsPrivateBitValid; } + + // used to indicate if child-side LoadContext * was null. + bool mIsNotNull; + // used to indicate if child-side mUsePrivateBrowsing flag is valid, even if + // mIsNotNull is false, i.e., child LoadContext was null. + bool mIsPrivateBitValid; + bool mIsContent; + bool mUseRemoteTabs; + bool mUseRemoteSubframes; + bool mUseTrackingProtection; + mozilla::OriginAttributes mOriginAttributes; +}; + +// Function to serialize over IPDL +template <> +struct ParamTraits<SerializedLoadContext> { + typedef SerializedLoadContext paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + nsAutoCString suffix; + aParam.mOriginAttributes.CreateSuffix(suffix); + + WriteParam(aWriter, aParam.mIsNotNull); + WriteParam(aWriter, aParam.mIsContent); + WriteParam(aWriter, aParam.mIsPrivateBitValid); + WriteParam(aWriter, aParam.mUseRemoteTabs); + WriteParam(aWriter, aParam.mUseRemoteSubframes); + WriteParam(aWriter, aParam.mUseTrackingProtection); + WriteParam(aWriter, suffix); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + nsAutoCString suffix; + if (!ReadParam(aReader, &aResult->mIsNotNull) || + !ReadParam(aReader, &aResult->mIsContent) || + !ReadParam(aReader, &aResult->mIsPrivateBitValid) || + !ReadParam(aReader, &aResult->mUseRemoteTabs) || + !ReadParam(aReader, &aResult->mUseRemoteSubframes) || + !ReadParam(aReader, &aResult->mUseTrackingProtection) || + !ReadParam(aReader, &suffix)) { + return false; + } + return aResult->mOriginAttributes.PopulateFromSuffix(suffix); + } +}; + +} // namespace IPC + +#endif // SerializedLoadContext_h diff --git a/docshell/base/SyncedContext.h b/docshell/base/SyncedContext.h new file mode 100644 index 0000000000..679e07edc2 --- /dev/null +++ b/docshell/base/SyncedContext.h @@ -0,0 +1,402 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_SyncedContext_h +#define mozilla_dom_SyncedContext_h + +#include <array> +#include <type_traits> +#include <utility> +#include "mozilla/Attributes.h" +#include "mozilla/BitSet.h" +#include "mozilla/EnumSet.h" +#include "nsStringFwd.h" +#include "nscore.h" + +// Referenced via macro definitions +#include "mozilla/ErrorResult.h" + +class PickleIterator; + +namespace IPC { +class Message; +class MessageReader; +class MessageWriter; +} // namespace IPC + +namespace mozilla { +namespace ipc { +class IProtocol; +class IPCResult; +template <typename T> +struct IPDLParamTraits; +} // namespace ipc + +namespace dom { +class ContentParent; +class ContentChild; +template <typename T> +class MaybeDiscarded; + +namespace syncedcontext { + +template <size_t I> +using Index = typename std::integral_constant<size_t, I>; + +// We're going to use the empty base optimization for synced fields of different +// sizes, so we define an empty class for that purpose. +template <size_t I, size_t S> +struct Empty {}; + +// A templated container for a synced field. I is the index and T is the type. +template <size_t I, typename T> +struct Field { + T mField{}; +}; + +// SizedField is a Field with a helper to define either an "empty" field, or a +// field of a given type. +template <size_t I, typename T, size_t S> +using SizedField = std::conditional_t<((sizeof(T) > 8) ? 8 : sizeof(T)) == S, + Field<I, T>, Empty<I, S>>; + +template <typename Context> +class Transaction { + public: + using IndexSet = EnumSet<size_t, BitSet<Context::FieldValues::count>>; + + // Set a field at the given index in this `Transaction`. Creating a + // `Transaction` object and setting multiple fields on it allows for + // multiple mutations to be performed atomically. + template <size_t I, typename U> + void Set(U&& aValue) { + mValues.Get(Index<I>{}) = std::forward<U>(aValue); + mModified += I; + } + + // Apply the changes from this transaction to the specified Context in all + // processes. This method will call the correct `CanSet` and `DidSet` methods, + // as well as move the value. + // + // If the target has been discarded, changes will be ignored. + // + // NOTE: This method mutates `this`, clearing the modified field set. + [[nodiscard]] nsresult Commit(Context* aOwner); + + // Called from `ContentParent` in response to a transaction from content. + mozilla::ipc::IPCResult CommitFromIPC(const MaybeDiscarded<Context>& aOwner, + ContentParent* aSource); + + // Called from `ContentChild` in response to a transaction from the parent. + mozilla::ipc::IPCResult CommitFromIPC(const MaybeDiscarded<Context>& aOwner, + uint64_t aEpoch, ContentChild* aSource); + + // Apply the changes from this transaction to the specified Context WITHOUT + // syncing the changes to other processes. + // + // Unlike `Commit`, this method will NOT call the corresponding `CanSet` or + // `DidSet` methods, and can be performed when the target context is + // unattached or discarded. + // + // NOTE: YOU PROBABLY DO NOT WANT TO USE THIS METHOD + void CommitWithoutSyncing(Context* aOwner); + + private: + friend struct mozilla::ipc::IPDLParamTraits<Transaction<Context>>; + + void Write(IPC::MessageWriter* aWriter, + mozilla::ipc::IProtocol* aActor) const; + bool Read(IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor); + + // You probably don't want to directly call this method - instead call + // `Commit`, which will perform the necessary synchronization. + // + // `Validate` must be called before calling this method. + void Apply(Context* aOwner, bool aFromIPC); + + // Returns the set of fields which failed to validate, or an empty set if + // there were no validation errors. + // + // NOTE: This method mutates `this` if any changes were reverted. + IndexSet Validate(Context* aOwner, ContentParent* aSource); + + template <typename F> + static void EachIndex(F&& aCallback) { + Context::FieldValues::EachIndex(aCallback); + } + + template <size_t I> + static uint64_t& FieldEpoch(Index<I>, Context* aContext) { + return std::get<I>(aContext->mFields.mEpochs); + } + + typename Context::FieldValues mValues; + IndexSet mModified; +}; + +template <typename Base, size_t Count> +class FieldValues : public Base { + public: + // The number of fields stored by this type. + static constexpr size_t count = Count; + + // The base type will define a series of `Get` methods for looking up a field + // by its field index. + using Base::Get; + + // Calls a generic lambda with an `Index<I>` for each index less than the + // field count. + template <typename F> + static void EachIndex(F&& aCallback) { + EachIndexInner(std::make_index_sequence<count>(), + std::forward<F>(aCallback)); + } + + private: + friend struct mozilla::ipc::IPDLParamTraits<FieldValues<Base, Count>>; + + void Write(IPC::MessageWriter* aWriter, + mozilla::ipc::IProtocol* aActor) const; + bool Read(IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor); + + template <typename F, size_t... Indexes> + static void EachIndexInner(std::index_sequence<Indexes...> aIndexes, + F&& aCallback) { + (aCallback(Index<Indexes>()), ...); + } +}; + +// Storage related to synchronized context fields. Contains both a tuple of +// individual field values, and epoch information for field synchronization. +template <typename Values> +class FieldStorage { + public: + // Unsafely grab a reference directly to the internal values structure which + // can be modified without telling other processes about the change. + // + // This is only sound in specific code which is already messaging other + // processes, and doesn't need to worry about epochs or other properties of + // field synchronization. + Values& RawValues() { return mValues; } + const Values& RawValues() const { return mValues; } + + // Get an individual field by index. + template <size_t I> + const auto& Get() const { + return RawValues().Get(Index<I>{}); + } + + // Set the value of a field without telling other processes about the change. + // + // This is only sound in specific code which is already messaging other + // processes, and doesn't need to worry about epochs or other properties of + // field synchronization. + template <size_t I, typename U> + void SetWithoutSyncing(U&& aValue) { + GetNonSyncingReference<I>() = std::move(aValue); + } + + // Get a reference to a field that can modify without telling other + // processes about the change. + // + // This is only sound in specific code which is already messaging other + // processes, and doesn't need to worry about epochs or other properties of + // field synchronization. + template <size_t I> + auto& GetNonSyncingReference() { + return RawValues().Get(Index<I>{}); + } + + FieldStorage() = default; + explicit FieldStorage(Values&& aInit) : mValues(std::move(aInit)) {} + + private: + template <typename Context> + friend class Transaction; + + // Data Members + std::array<uint64_t, Values::count> mEpochs{}; + Values mValues; +}; + +// Alternative return type enum for `CanSet` validators which allows specifying +// more behaviour. +enum class CanSetResult : uint8_t { + // The set attempt is denied. This is equivalent to returning `false`. + Deny, + // The set attempt is allowed. This is equivalent to returning `true`. + Allow, + // The set attempt is reverted non-fatally. + Revert, +}; + +// Helper type traits to use concrete types rather than generic forwarding +// references for the `SetXXX` methods defined on the synced context type. +// +// This helps avoid potential issues where someone accidentally declares an +// overload of these methods with slightly different types and different +// behaviours. See bug 1659520. +template <typename T> +struct GetFieldSetterType { + using SetterArg = T; +}; +template <> +struct GetFieldSetterType<nsString> { + using SetterArg = const nsAString&; +}; +template <> +struct GetFieldSetterType<nsCString> { + using SetterArg = const nsACString&; +}; +template <typename T> +using FieldSetterType = typename GetFieldSetterType<T>::SetterArg; + +#define MOZ_DECL_SYNCED_CONTEXT_FIELD_INDEX(name, type) IDX_##name, + +#define MOZ_DECL_SYNCED_CONTEXT_FIELD_GETSET(name, type) \ + const type& Get##name() const { return mFields.template Get<IDX_##name>(); } \ + \ + [[nodiscard]] nsresult Set##name( \ + ::mozilla::dom::syncedcontext::FieldSetterType<type> aValue) { \ + Transaction txn; \ + txn.template Set<IDX_##name>(std::move(aValue)); \ + return txn.Commit(this); \ + } \ + void Set##name(::mozilla::dom::syncedcontext::FieldSetterType<type> aValue, \ + ErrorResult& aRv) { \ + nsresult rv = this->Set##name(std::move(aValue)); \ + if (NS_FAILED(rv)) { \ + aRv.ThrowInvalidStateError("cannot set synced field '" #name \ + "': context is discarded"); \ + } \ + } + +#define MOZ_DECL_SYNCED_CONTEXT_TRANSACTION_SET(name, type) \ + template <typename U> \ + void Set##name(U&& aValue) { \ + this->template Set<IDX_##name>(std::forward<U>(aValue)); \ + } +#define MOZ_DECL_SYNCED_CONTEXT_INDEX_TO_NAME(name, type) \ + case IDX_##name: \ + return #name; + +#define MOZ_DECL_SYNCED_FIELD_INHERIT(name, type) \ + public \ + syncedcontext::SizedField<IDX_##name, type, Size>, + +#define MOZ_DECL_SYNCED_CONTEXT_BASE_FIELD_GETTER(name, type) \ + type& Get(FieldIndex<IDX_##name>) { \ + return Field<IDX_##name, type>::mField; \ + } \ + const type& Get(FieldIndex<IDX_##name>) const { \ + return Field<IDX_##name, type>::mField; \ + } + +// Declare a type as a synced context type. +// +// clazz is the name of the type being declared, and `eachfield` is a macro +// which, when called with the name of the macro, will call that macro once for +// each field in the synced context. +#define MOZ_DECL_SYNCED_CONTEXT(clazz, eachfield) \ + public: \ + /* Index constants for referring to each field in generic code */ \ + enum FieldIndexes { \ + eachfield(MOZ_DECL_SYNCED_CONTEXT_FIELD_INDEX) SYNCED_FIELD_COUNT \ + }; \ + \ + /* Helper for overloading methods like `CanSet` and `DidSet` */ \ + template <size_t I> \ + using FieldIndex = typename ::mozilla::dom::syncedcontext::Index<I>; \ + \ + /* Fields contain all synced fields defined by \ + * `eachfield(MOZ_DECL_SYNCED_FIELD_INHERIT)`, but only those where the size \ + * of the field is equal to size Size will be present. We use SizedField to \ + * remove fields of the wrong size. */ \ + template <size_t Size> \ + struct Fields : eachfield(MOZ_DECL_SYNCED_FIELD_INHERIT) \ + syncedcontext::Empty<SYNCED_FIELD_COUNT, Size> {}; \ + \ + /* Struct containing the data for all synced fields as members. We filter \ + * sizes to lay out fields of size 1, then 2, then 4 and last 8 or greater. \ + * This way we don't need to consider packing when defining fields, but \ + * we'll just reorder them here. \ + */ \ + struct BaseFieldValues : public Fields<1>, \ + public Fields<2>, \ + public Fields<4>, \ + public Fields<8> { \ + template <size_t I> \ + auto& Get() { \ + return Get(FieldIndex<I>{}); \ + } \ + template <size_t I> \ + const auto& Get() const { \ + return Get(FieldIndex<I>{}); \ + } \ + eachfield(MOZ_DECL_SYNCED_CONTEXT_BASE_FIELD_GETTER) \ + }; \ + using FieldValues = \ + typename ::mozilla::dom::syncedcontext::FieldValues<BaseFieldValues, \ + SYNCED_FIELD_COUNT>; \ + \ + protected: \ + friend class ::mozilla::dom::syncedcontext::Transaction<clazz>; \ + ::mozilla::dom::syncedcontext::FieldStorage<FieldValues> mFields; \ + \ + public: \ + /* Transaction types for bulk mutations */ \ + using BaseTransaction = ::mozilla::dom::syncedcontext::Transaction<clazz>; \ + class Transaction final : public BaseTransaction { \ + public: \ + eachfield(MOZ_DECL_SYNCED_CONTEXT_TRANSACTION_SET) \ + }; \ + \ + /* Field name getter by field index */ \ + static const char* FieldIndexToName(size_t aIndex) { \ + switch (aIndex) { eachfield(MOZ_DECL_SYNCED_CONTEXT_INDEX_TO_NAME) } \ + return "<unknown>"; \ + } \ + eachfield(MOZ_DECL_SYNCED_CONTEXT_FIELD_GETSET) + +} // namespace syncedcontext +} // namespace dom + +namespace ipc { + +template <typename Context> +struct IPDLParamTraits<dom::syncedcontext::Transaction<Context>> { + typedef dom::syncedcontext::Transaction<Context> paramType; + + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const paramType& aParam) { + aParam.Write(aWriter, aActor); + } + + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + paramType* aResult) { + return aResult->Read(aReader, aActor); + } +}; + +template <typename Base, size_t Count> +struct IPDLParamTraits<dom::syncedcontext::FieldValues<Base, Count>> { + typedef dom::syncedcontext::FieldValues<Base, Count> paramType; + + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const paramType& aParam) { + aParam.Write(aWriter, aActor); + } + + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + paramType* aResult) { + return aResult->Read(aReader, aActor); + } +}; + +} // namespace ipc +} // namespace mozilla + +#endif // !defined(mozilla_dom_SyncedContext_h) diff --git a/docshell/base/SyncedContextInlines.h b/docshell/base/SyncedContextInlines.h new file mode 100644 index 0000000000..99141d630d --- /dev/null +++ b/docshell/base/SyncedContextInlines.h @@ -0,0 +1,358 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_SyncedContextInlines_h +#define mozilla_dom_SyncedContextInlines_h + +#include "mozilla/dom/SyncedContext.h" +#include "mozilla/dom/BrowsingContextGroup.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ContentChild.h" +#include "nsReadableUtils.h" +#include "mozilla/HalIPCUtils.h" + +namespace mozilla { +namespace dom { +namespace syncedcontext { + +template <typename T> +struct IsMozillaMaybe : std::false_type {}; +template <typename T> +struct IsMozillaMaybe<Maybe<T>> : std::true_type {}; + +// A super-sketchy logging only function for generating a human-readable version +// of the value of a synced context field. +template <typename T> +void FormatFieldValue(nsACString& aStr, const T& aValue) { + if constexpr (std::is_same_v<bool, T>) { + if (aValue) { + aStr.AppendLiteral("true"); + } else { + aStr.AppendLiteral("false"); + } + } else if constexpr (std::is_same_v<nsID, T>) { + aStr.Append(nsIDToCString(aValue).get()); + } else if constexpr (std::is_integral_v<T>) { + aStr.AppendInt(aValue); + } else if constexpr (std::is_enum_v<T>) { + aStr.AppendInt(static_cast<std::underlying_type_t<T>>(aValue)); + } else if constexpr (std::is_floating_point_v<T>) { + aStr.AppendFloat(aValue); + } else if constexpr (std::is_base_of_v<nsAString, T>) { + AppendUTF16toUTF8(aValue, aStr); + } else if constexpr (std::is_base_of_v<nsACString, T>) { + aStr.Append(aValue); + } else if constexpr (IsMozillaMaybe<T>::value) { + if (aValue) { + aStr.AppendLiteral("Some("); + FormatFieldValue(aStr, aValue.ref()); + aStr.AppendLiteral(")"); + } else { + aStr.AppendLiteral("Nothing"); + } + } else { + aStr.AppendLiteral("???"); + } +} + +// Sketchy logging-only logic to generate a human-readable output of the actions +// a synced context transaction is going to perform. +template <typename Context> +nsAutoCString FormatTransaction( + typename Transaction<Context>::IndexSet aModified, + const typename Context::FieldValues& aOldValues, + const typename Context::FieldValues& aNewValues) { + nsAutoCString result; + Context::FieldValues::EachIndex([&](auto idx) { + if (aModified.contains(idx)) { + result.Append(Context::FieldIndexToName(idx)); + result.AppendLiteral("("); + FormatFieldValue(result, aOldValues.Get(idx)); + result.AppendLiteral("->"); + FormatFieldValue(result, aNewValues.Get(idx)); + result.AppendLiteral(") "); + } + }); + return result; +} + +template <typename Context> +nsCString FormatValidationError( + typename Transaction<Context>::IndexSet aFailedFields, const char* prefix) { + MOZ_ASSERT(!aFailedFields.isEmpty()); + return nsDependentCString{prefix} + + StringJoin(", "_ns, aFailedFields, + [](nsACString& dest, const auto& idx) { + dest.Append(Context::FieldIndexToName(idx)); + }); +} + +template <typename Context> +nsresult Transaction<Context>::Commit(Context* aOwner) { + if (NS_WARN_IF(aOwner->IsDiscarded())) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + IndexSet failedFields = Validate(aOwner, nullptr); + if (!failedFields.isEmpty()) { + nsCString error = FormatValidationError<Context>( + failedFields, "CanSet failed for field(s): "); + MOZ_CRASH_UNSAFE_PRINTF("%s", error.get()); + } + + if (mModified.isEmpty()) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + + // Increment the field epoch for fields affected by this transaction. + uint64_t epoch = cc->NextBrowsingContextFieldEpoch(); + EachIndex([&](auto idx) { + if (mModified.contains(idx)) { + FieldEpoch(idx, aOwner) = epoch; + } + }); + + // Tell our derived class to send the correct "Commit" IPC message. + aOwner->SendCommitTransaction(cc, *this, epoch); + } else { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + // Tell our derived class to send the correct "Commit" IPC messages. + BrowsingContextGroup* group = aOwner->Group(); + group->EachParent([&](ContentParent* aParent) { + aOwner->SendCommitTransaction(aParent, *this, + aParent->GetBrowsingContextFieldEpoch()); + }); + } + + Apply(aOwner, /* aFromIPC */ false); + return NS_OK; +} + +template <typename Context> +mozilla::ipc::IPCResult Transaction<Context>::CommitFromIPC( + const MaybeDiscarded<Context>& aOwner, ContentParent* aSource) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + if (aOwner.IsNullOrDiscarded()) { + MOZ_LOG(Context::GetSyncLog(), LogLevel::Debug, + ("IPC: Trying to send a message to dead or detached context")); + return IPC_OK(); + } + Context* owner = aOwner.get(); + + // Validate that the set from content is allowed before continuing. + IndexSet failedFields = Validate(owner, aSource); + if (!failedFields.isEmpty()) { + nsCString error = FormatValidationError<Context>( + failedFields, + "Invalid Transaction from Child - CanSet failed for field(s): "); + return IPC_FAIL(aSource, error.get()); + } + + // Validate may have dropped some fields from the transaction, check it's not + // empty before continuing. + if (mModified.isEmpty()) { + return IPC_OK(); + } + + BrowsingContextGroup* group = owner->Group(); + group->EachOtherParent(aSource, [&](ContentParent* aParent) { + owner->SendCommitTransaction(aParent, *this, + aParent->GetBrowsingContextFieldEpoch()); + }); + + Apply(owner, /* aFromIPC */ true); + return IPC_OK(); +} + +template <typename Context> +mozilla::ipc::IPCResult Transaction<Context>::CommitFromIPC( + const MaybeDiscarded<Context>& aOwner, uint64_t aEpoch, + ContentChild* aSource) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess()); + if (aOwner.IsNullOrDiscarded()) { + MOZ_LOG(Context::GetSyncLog(), LogLevel::Debug, + ("ChildIPC: Trying to send a message to dead or detached context")); + return IPC_OK(); + } + Context* owner = aOwner.get(); + + // Clear any fields which have been obsoleted by the epoch. + EachIndex([&](auto idx) { + if (mModified.contains(idx) && FieldEpoch(idx, owner) > aEpoch) { + MOZ_LOG( + Context::GetSyncLog(), LogLevel::Debug, + ("Transaction::Obsoleted(#%" PRIx64 ", %" PRIu64 ">%" PRIu64 "): %s", + owner->Id(), FieldEpoch(idx, owner), aEpoch, + Context::FieldIndexToName(idx))); + mModified -= idx; + } + }); + + if (mModified.isEmpty()) { + return IPC_OK(); + } + + Apply(owner, /* aFromIPC */ true); + return IPC_OK(); +} + +template <typename Context> +void Transaction<Context>::Apply(Context* aOwner, bool aFromIPC) { + MOZ_ASSERT(!mModified.isEmpty()); + + MOZ_LOG( + Context::GetSyncLog(), LogLevel::Debug, + ("Transaction::Apply(#%" PRIx64 ", %s): %s", aOwner->Id(), + aFromIPC ? "ipc" : "local", + FormatTransaction<Context>(mModified, aOwner->mFields.mValues, mValues) + .get())); + + EachIndex([&](auto idx) { + if (mModified.contains(idx)) { + auto& txnField = mValues.Get(idx); + auto& ownerField = aOwner->mFields.mValues.Get(idx); + std::swap(ownerField, txnField); + aOwner->DidSet(idx); + aOwner->DidSet(idx, std::move(txnField)); + } + }); + mModified.clear(); +} + +template <typename Context> +void Transaction<Context>::CommitWithoutSyncing(Context* aOwner) { + MOZ_LOG( + Context::GetSyncLog(), LogLevel::Debug, + ("Transaction::CommitWithoutSyncing(#%" PRIx64 "): %s", aOwner->Id(), + FormatTransaction<Context>(mModified, aOwner->mFields.mValues, mValues) + .get())); + + EachIndex([&](auto idx) { + if (mModified.contains(idx)) { + aOwner->mFields.mValues.Get(idx) = std::move(mValues.Get(idx)); + } + }); + mModified.clear(); +} + +inline CanSetResult AsCanSetResult(CanSetResult aValue) { return aValue; } +inline CanSetResult AsCanSetResult(bool aValue) { + return aValue ? CanSetResult::Allow : CanSetResult::Deny; +} + +template <typename Context> +typename Transaction<Context>::IndexSet Transaction<Context>::Validate( + Context* aOwner, ContentParent* aSource) { + IndexSet failedFields; + Transaction<Context> revertTxn; + + EachIndex([&](auto idx) { + if (!mModified.contains(idx)) { + return; + } + + switch (AsCanSetResult(aOwner->CanSet(idx, mValues.Get(idx), aSource))) { + case CanSetResult::Allow: + break; + case CanSetResult::Deny: + failedFields += idx; + break; + case CanSetResult::Revert: + revertTxn.mValues.Get(idx) = aOwner->mFields.mValues.Get(idx); + revertTxn.mModified += idx; + break; + } + }); + + // If any changes need to be reverted, log them, remove them from this + // transaction, and optionally send a message to the source process. + if (!revertTxn.mModified.isEmpty()) { + // NOTE: Logging with modified IndexSet from revert transaction, and values + // from this transaction, so we log the failed values we're going to revert. + MOZ_LOG( + Context::GetSyncLog(), LogLevel::Debug, + ("Transaction::PartialRevert(#%" PRIx64 ", pid %" PRIPID "): %s", + aOwner->Id(), aSource ? aSource->OtherPid() : base::kInvalidProcessId, + FormatTransaction<Context>(revertTxn.mModified, mValues, + revertTxn.mValues) + .get())); + + mModified -= revertTxn.mModified; + + if (aSource) { + aOwner->SendCommitTransaction(aSource, revertTxn, + aSource->GetBrowsingContextFieldEpoch()); + } + } + return failedFields; +} + +template <typename Context> +void Transaction<Context>::Write(IPC::MessageWriter* aWriter, + mozilla::ipc::IProtocol* aActor) const { + // Record which field indices will be included, and then write those fields + // out. + typename IndexSet::serializedType modified = mModified.serialize(); + WriteIPDLParam(aWriter, aActor, modified); + EachIndex([&](auto idx) { + if (mModified.contains(idx)) { + WriteIPDLParam(aWriter, aActor, mValues.Get(idx)); + } + }); +} + +template <typename Context> +bool Transaction<Context>::Read(IPC::MessageReader* aReader, + mozilla::ipc::IProtocol* aActor) { + // Read in which field indices were sent by the remote, followed by the fields + // identified by those indices. + typename IndexSet::serializedType modified = + typename IndexSet::serializedType{}; + if (!ReadIPDLParam(aReader, aActor, &modified)) { + return false; + } + mModified.deserialize(modified); + + bool ok = true; + EachIndex([&](auto idx) { + if (ok && mModified.contains(idx)) { + ok = ReadIPDLParam(aReader, aActor, &mValues.Get(idx)); + } + }); + return ok; +} + +template <typename Base, size_t Count> +void FieldValues<Base, Count>::Write(IPC::MessageWriter* aWriter, + mozilla::ipc::IProtocol* aActor) const { + // XXX The this-> qualification is necessary to work around a bug in older gcc + // versions causing an ICE. + EachIndex([&](auto idx) { WriteIPDLParam(aWriter, aActor, this->Get(idx)); }); +} + +template <typename Base, size_t Count> +bool FieldValues<Base, Count>::Read(IPC::MessageReader* aReader, + mozilla::ipc::IProtocol* aActor) { + bool ok = true; + EachIndex([&](auto idx) { + if (ok) { + // XXX The this-> qualification is necessary to work around a bug in older + // gcc versions causing an ICE. + ok = ReadIPDLParam(aReader, aActor, &this->Get(idx)); + } + }); + return ok; +} + +} // namespace syncedcontext +} // namespace dom +} // namespace mozilla + +#endif // !defined(mozilla_dom_SyncedContextInlines_h) diff --git a/docshell/base/URIFixup.sys.mjs b/docshell/base/URIFixup.sys.mjs new file mode 100644 index 0000000000..212bb0ddbf --- /dev/null +++ b/docshell/base/URIFixup.sys.mjs @@ -0,0 +1,1303 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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 component handles fixing up URIs, by correcting obvious typos and adding + * missing schemes. + * URI references: + * http://www.faqs.org/rfcs/rfc1738.html + * http://www.faqs.org/rfcs/rfc2396.html + */ + +// TODO (Bug 1641220) getFixupURIInfo has a complex logic, that likely could be +// simplified, but the risk of regressing its behavior is high. +/* eslint complexity: ["error", 43] */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "externalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "defaultProtocolHandler", + "@mozilla.org/network/protocol;1?name=default", + "nsIProtocolHandler" +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "fileProtocolHandler", + "@mozilla.org/network/protocol;1?name=file", + "nsIFileProtocolHandler" +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "handlerService", + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService" +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "fixupSchemeTypos", + "browser.fixup.typo.scheme", + true +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "dnsFirstForSingleWords", + "browser.fixup.dns_first_for_single_words", + false +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "keywordEnabled", + "keyword.enabled", + true +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "alternateEnabled", + "browser.fixup.alternate.enabled", + false +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "alternateProtocol", + "browser.fixup.alternate.protocol", + "https" +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "dnsResolveFullyQualifiedNames", + "browser.urlbar.dnsResolveFullyQualifiedNames", + true +); + +const { + FIXUP_FLAG_NONE, + FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP, + FIXUP_FLAGS_MAKE_ALTERNATE_URI, + FIXUP_FLAG_PRIVATE_CONTEXT, + FIXUP_FLAG_FIX_SCHEME_TYPOS, + FIXUP_FLAG_FORCE_ALTERNATE_URI, +} = Ci.nsIURIFixup; + +const COMMON_PROTOCOLS = ["http", "https", "file"]; + +// Regex used to identify user:password tokens in url strings. +// This is not a strict valid characters check, because we try to fixup this +// part of the url too. +XPCOMUtils.defineLazyGetter( + lazy, + "userPasswordRegex", + () => /^([a-z+.-]+:\/{0,3})*([^\/@]+@).+/i +); + +// Regex used to identify the string that starts with port expression. +XPCOMUtils.defineLazyGetter(lazy, "portRegex", () => /^:\d{1,5}([?#/]|$)/); + +// Regex used to identify numbers. +XPCOMUtils.defineLazyGetter(lazy, "numberRegex", () => /^[0-9]+(\.[0-9]+)?$/); + +// Regex used to identify tab separated content (having at least 2 tabs). +XPCOMUtils.defineLazyGetter(lazy, "maxOneTabRegex", () => /^[^\t]*\t?[^\t]*$/); + +// Regex used to test if a string with a protocol might instead be a url +// without a protocol but with a port: +// +// <hostname>:<port> or +// <hostname>:<port>/ +// +// Where <hostname> is a string of alphanumeric characters and dashes +// separated by dots. +// and <port> is a 5 or less digits. This actually breaks the rfc2396 +// definition of a scheme which allows dots in schemes. +// +// Note: +// People expecting this to work with +// <user>:<password>@<host>:<port>/<url-path> will be disappointed! +// +// Note: Parser could be a lot tighter, tossing out silly hostnames +// such as those containing consecutive dots and so on. +XPCOMUtils.defineLazyGetter( + lazy, + "possiblyHostPortRegex", + () => /^[a-z0-9-]+(\.[a-z0-9-]+)*:[0-9]{1,5}([/?#]|$)/i +); + +// Regex used to strip newlines. +XPCOMUtils.defineLazyGetter(lazy, "newLinesRegex", () => /[\r\n]/g); + +// Regex used to match a possible protocol. +// This resembles the logic in Services.io.extractScheme, thus \t is admitted +// and stripped later. We don't use Services.io.extractScheme because of +// performance bottleneck caused by crossing XPConnect. +XPCOMUtils.defineLazyGetter( + lazy, + "possibleProtocolRegex", + () => /^([a-z][a-z0-9.+\t-]*)(:|;)?(\/\/)?/i +); + +// Regex used to match IPs. Note that these are not made to validate IPs, but +// just to detect strings that look like an IP. They also skip protocol. +// For IPv4 this also accepts a shorthand format with just 2 dots. +XPCOMUtils.defineLazyGetter( + lazy, + "IPv4LikeRegex", + () => /^(?:[a-z+.-]+:\/*(?!\/))?(?:\d{1,3}\.){2,3}\d{1,3}(?::\d+|\/)?/i +); +XPCOMUtils.defineLazyGetter( + lazy, + "IPv6LikeRegex", + () => + /^(?:[a-z+.-]+:\/*(?!\/))?\[(?:[0-9a-f]{0,4}:){0,7}[0-9a-f]{0,4}\]?(?::\d+|\/)?/i +); + +// Cache of known domains. +XPCOMUtils.defineLazyGetter(lazy, "knownDomains", () => { + const branch = "browser.fixup.domainwhitelist."; + let domains = new Set( + Services.prefs + .getChildList(branch) + .filter(p => Services.prefs.getBoolPref(p, false)) + .map(p => p.substring(branch.length)) + ); + // Hold onto the observer to avoid it being GC-ed. + domains._observer = { + observe(subject, topic, data) { + let domain = data.substring(branch.length); + if (Services.prefs.getBoolPref(data, false)) { + domains.add(domain); + } else { + domains.delete(domain); + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + }; + Services.prefs.addObserver(branch, domains._observer, true); + return domains; +}); + +// Cache of known suffixes. +// This works differently from the known domains, because when we examine a +// domain we can't tell how many dot-separated parts constitute the suffix. +// We create a Map keyed by the last dotted part, containing a Set of +// all the suffixes ending with that part: +// "two" => ["two"] +// "three" => ["some.three", "three"] +// When searching we can restrict the linear scan based on the last part. +// The ideal structure for this would be a Directed Acyclic Word Graph, but +// since we expect this list to be small it's not worth the complication. +XPCOMUtils.defineLazyGetter(lazy, "knownSuffixes", () => { + const branch = "browser.fixup.domainsuffixwhitelist."; + let suffixes = new Map(); + let prefs = Services.prefs + .getChildList(branch) + .filter(p => Services.prefs.getBoolPref(p, false)); + for (let pref of prefs) { + let suffix = pref.substring(branch.length); + let lastPart = suffix.substr(suffix.lastIndexOf(".") + 1); + if (lastPart) { + let entries = suffixes.get(lastPart); + if (!entries) { + entries = new Set(); + suffixes.set(lastPart, entries); + } + entries.add(suffix); + } + } + // Hold onto the observer to avoid it being GC-ed. + suffixes._observer = { + observe(subject, topic, data) { + let suffix = data.substring(branch.length); + let lastPart = suffix.substr(suffix.lastIndexOf(".") + 1); + let entries = suffixes.get(lastPart); + if (Services.prefs.getBoolPref(data, false)) { + // Add the suffix. + if (!entries) { + entries = new Set(); + suffixes.set(lastPart, entries); + } + entries.add(suffix); + } else if (entries) { + // Remove the suffix. + entries.delete(suffix); + if (!entries.size) { + suffixes.delete(lastPart); + } + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + }; + Services.prefs.addObserver(branch, suffixes._observer, true); + return suffixes; +}); + +export function URIFixup() { + // There are cases that nsIExternalProtocolService.externalProtocolHandlerExists() does + // not work well and returns always true due to flatpak. In this case, in order to + // fallback to nsIHandlerService.exits(), we test whether can trust + // nsIExternalProtocolService here. + this._trustExternalProtocolService = + !lazy.externalProtocolService.externalProtocolHandlerExists( + `__dummy${Date.now()}__` + ); +} + +URIFixup.prototype = { + get FIXUP_FLAG_NONE() { + return FIXUP_FLAG_NONE; + }, + get FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP() { + return FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + }, + get FIXUP_FLAGS_MAKE_ALTERNATE_URI() { + return FIXUP_FLAGS_MAKE_ALTERNATE_URI; + }, + get FIXUP_FLAG_FORCE_ALTERNATE_URI() { + return FIXUP_FLAG_FORCE_ALTERNATE_URI; + }, + get FIXUP_FLAG_PRIVATE_CONTEXT() { + return FIXUP_FLAG_PRIVATE_CONTEXT; + }, + get FIXUP_FLAG_FIX_SCHEME_TYPOS() { + return FIXUP_FLAG_FIX_SCHEME_TYPOS; + }, + + getFixupURIInfo(uriString, fixupFlags = FIXUP_FLAG_NONE) { + let isPrivateContext = fixupFlags & FIXUP_FLAG_PRIVATE_CONTEXT; + + // Eliminate embedded newlines, which single-line text fields now allow, + // and cleanup the empty spaces and tabs that might be on each end. + uriString = uriString.trim().replace(lazy.newLinesRegex, ""); + + if (!uriString) { + throw new Components.Exception( + "Should pass a non-null uri", + Cr.NS_ERROR_FAILURE + ); + } + + let info = new URIFixupInfo(uriString); + + const { scheme, fixedSchemeUriString, fixupChangedProtocol } = + extractScheme(uriString, fixupFlags); + uriString = fixedSchemeUriString; + info.fixupChangedProtocol = fixupChangedProtocol; + + if (scheme == "view-source") { + let { preferredURI, postData } = fixupViewSource(uriString, fixupFlags); + info.preferredURI = info.fixedURI = preferredURI; + info.postData = postData; + return info; + } + + if (scheme.length < 2) { + // Check if it is a file path. We skip most schemes because the only case + // where a file path may look like having a scheme is "X:" on Windows. + let fileURI = fileURIFixup(uriString); + if (fileURI) { + info.preferredURI = info.fixedURI = fileURI; + info.fixupChangedProtocol = true; + return info; + } + } + + const isCommonProtocol = COMMON_PROTOCOLS.includes(scheme); + + let canHandleProtocol = + scheme && + (isCommonProtocol || + Services.io.getProtocolHandler(scheme) != lazy.defaultProtocolHandler || + this._isKnownExternalProtocol(scheme)); + + if ( + canHandleProtocol || + // If it's an unknown handler and the given URL looks like host:port or + // has a user:password we can't pass it to the external protocol handler. + // We'll instead try fixing it with http later. + (!lazy.possiblyHostPortRegex.test(uriString) && + !lazy.userPasswordRegex.test(uriString)) + ) { + // Just try to create an URL out of it. + try { + info.fixedURI = Services.io.newURI(uriString); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_MALFORMED_URI) { + throw ex; + } + } + } + + // We're dealing with a theoretically valid URI but we have no idea how to + // load it. (e.g. "christmas:humbug") + // It's more likely the user wants to search, and so we chuck this over to + // their preferred search provider. + // TODO (Bug 1588118): Should check FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP + // instead of FIXUP_FLAG_FIX_SCHEME_TYPOS. + if ( + info.fixedURI && + lazy.keywordEnabled && + fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS && + scheme && + !canHandleProtocol + ) { + tryKeywordFixupForURIInfo(uriString, info, isPrivateContext); + } + + if (info.fixedURI) { + if (!info.preferredURI) { + maybeSetAlternateFixedURI(info, fixupFlags); + info.preferredURI = info.fixedURI; + } + fixupConsecutiveDotsHost(info); + return info; + } + + // Fix up protocol string before calling KeywordURIFixup, because + // it cares about the hostname of such URIs. + // Prune duff protocol schemes: + // ://totallybroken.url.com + // //shorthand.url.com + let inputHadDuffProtocol = + uriString.startsWith("://") || uriString.startsWith("//"); + if (inputHadDuffProtocol) { + uriString = uriString.replace(/^:?\/\//, ""); + } + + // Avoid fixing up content that looks like tab-separated values. + // Assume that 1 tab is accidental, but more than 1 implies this is + // supposed to be tab-separated content. + if (!isCommonProtocol && lazy.maxOneTabRegex.test(uriString)) { + let uriWithProtocol = fixupURIProtocol(uriString); + if (uriWithProtocol) { + info.fixedURI = uriWithProtocol; + info.fixupChangedProtocol = true; + maybeSetAlternateFixedURI(info, fixupFlags); + info.preferredURI = info.fixedURI; + // Check if it's a forced visit. The user can enforce a visit by + // appending a slash, but the string must be in a valid uri format. + if (uriString.endsWith("/")) { + fixupConsecutiveDotsHost(info); + return info; + } + } + } + + // Handle "www.<something>" as a URI. + const asciiHost = info.fixedURI?.asciiHost; + if ( + asciiHost?.length > 4 && + asciiHost?.startsWith("www.") && + asciiHost?.lastIndexOf(".") == 3 + ) { + return info; + } + + // Memoize the public suffix check, since it may be expensive and should + // only run once when necessary. + let suffixInfo; + function checkSuffix(info) { + if (!suffixInfo) { + suffixInfo = checkAndFixPublicSuffix(info); + } + return suffixInfo; + } + + // See if it is a keyword and whether a keyword must be fixed up. + if ( + lazy.keywordEnabled && + fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP && + !inputHadDuffProtocol && + !checkSuffix(info).suffix && + keywordURIFixup(uriString, info, isPrivateContext) + ) { + fixupConsecutiveDotsHost(info); + return info; + } + + if ( + info.fixedURI && + (!info.fixupChangedProtocol || !checkSuffix(info).hasUnknownSuffix) + ) { + fixupConsecutiveDotsHost(info); + return info; + } + + // If we still haven't been able to construct a valid URI, try to force a + // keyword match. + if (lazy.keywordEnabled && fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP) { + tryKeywordFixupForURIInfo(info.originalInput, info, isPrivateContext); + } + + if (!info.preferredURI) { + // We couldn't salvage anything. + throw new Components.Exception( + "Couldn't build a valid uri", + Cr.NS_ERROR_MALFORMED_URI + ); + } + + fixupConsecutiveDotsHost(info); + return info; + }, + + webNavigationFlagsToFixupFlags(href, navigationFlags) { + try { + Services.io.newURI(href); + // Remove LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP for valid uris. + navigationFlags &= + ~Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + } catch (ex) {} + + let fixupFlags = FIXUP_FLAG_NONE; + if ( + navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP + ) { + fixupFlags |= FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + } + if (navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS) { + fixupFlags |= FIXUP_FLAG_FIX_SCHEME_TYPOS; + } + return fixupFlags; + }, + + keywordToURI(keyword, isPrivateContext) { + if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + // There's no search service in the content process, thus all the calls + // from it that care about keywords conversion should go through the + // parent process. + throw new Components.Exception( + "Can't invoke URIFixup in the content process", + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + let info = new URIFixupInfo(keyword); + + // Strip leading "?" and leading/trailing spaces from aKeyword + if (keyword.startsWith("?")) { + keyword = keyword.substring(1); + } + keyword = keyword.trim(); + + if (!Services.search.hasSuccessfullyInitialized) { + return info; + } + + // Try falling back to the search service's default search engine + // We must use an appropriate search engine depending on the private + // context. + let engine = isPrivateContext + ? Services.search.defaultPrivateEngine + : Services.search.defaultEngine; + + // We allow default search plugins to specify alternate parameters that are + // specific to keyword searches. + let responseType = null; + if (engine.supportsResponseType("application/x-moz-keywordsearch")) { + responseType = "application/x-moz-keywordsearch"; + } + let submission = engine.getSubmission(keyword, responseType, "keyword"); + if ( + !submission || + // For security reasons (avoid redirecting to file, data, or other unsafe + // protocols) we only allow fixup to http/https search engines. + !submission.uri.scheme.startsWith("http") + ) { + throw new Components.Exception( + "Invalid search submission uri", + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + let submissionPostDataStream = submission.postData; + if (submissionPostDataStream) { + info.postData = submissionPostDataStream; + } + + info.keywordProviderName = engine.name; + info.keywordAsSent = keyword; + info.preferredURI = submission.uri; + return info; + }, + + forceHttpFixup(uriString) { + if (!uriString) { + throw new Components.Exception( + "Should pass a non-null uri", + Cr.NS_ERROR_FAILURE + ); + } + + let info = new URIFixupInfo(uriString); + let { scheme, fixedSchemeUriString, fixupChangedProtocol } = extractScheme( + uriString, + FIXUP_FLAG_FIX_SCHEME_TYPOS + ); + + if (scheme != "http" && scheme != "https") { + throw new Components.Exception( + "Scheme should be either http or https", + Cr.NS_ERROR_FAILURE + ); + } + + info.fixupChangedProtocol = fixupChangedProtocol; + info.fixedURI = Services.io.newURI(fixedSchemeUriString); + + let host = info.fixedURI.host; + if (host != "http" && host != "https" && host != "localhost") { + let modifiedHostname = maybeAddPrefixAndSuffix(host); + updateHostAndScheme(info, modifiedHostname); + info.preferredURI = info.fixedURI; + } + + return info; + }, + + checkHost(uri, listener, originAttributes) { + let { displayHost, asciiHost } = uri; + if (!displayHost) { + throw new Components.Exception( + "URI must have displayHost", + Cr.NS_ERROR_FAILURE + ); + } + if (!asciiHost) { + throw new Components.Exception( + "URI must have asciiHost", + Cr.NS_ERROR_FAILURE + ); + } + + let isIPv4Address = host => { + let parts = host.split("."); + if (parts.length != 4) { + return false; + } + return parts.every(part => { + let n = parseInt(part, 10); + return n >= 0 && n <= 255; + }); + }; + + // Avoid showing fixup information if we're suggesting an IP. Note that + // decimal representations of IPs are normalized to a 'regular' + // dot-separated IP address by network code, but that only happens for + // numbers that don't overflow. Longer numbers do not get normalized, + // but still work to access IP addresses. So for instance, + // 1097347366913 (ff7f000001) gets resolved by using the final bytes, + // making it the same as 7f000001, which is 127.0.0.1 aka localhost. + // While 2130706433 would get normalized by network, 1097347366913 + // does not, and we have to deal with both cases here: + if (isIPv4Address(asciiHost) || /^(?:\d+|0x[a-f0-9]+)$/i.test(asciiHost)) { + return; + } + + // For dotless hostnames, we want to ensure this ends with a '.' but don't + // want the . showing up in the UI if we end up notifying the user, so we + // use a separate variable. + let lookupName = displayHost; + if (lazy.dnsResolveFullyQualifiedNames && !lookupName.includes(".")) { + lookupName += "."; + } + + Services.obs.notifyObservers(null, "uri-fixup-check-dns"); + Services.dns.asyncResolve( + lookupName, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + Services.tm.mainThread, + originAttributes + ); + }, + + isDomainKnown, + + _isKnownExternalProtocol(scheme) { + if (this._trustExternalProtocolService) { + return lazy.externalProtocolService.externalProtocolHandlerExists(scheme); + } + + try { + // nsIExternalProtocolService.getProtocolHandlerInfo() on Android throws + // error due to not implemented. + return lazy.handlerService.exists( + lazy.externalProtocolService.getProtocolHandlerInfo(scheme) + ); + } catch (e) { + return false; + } + }, + + classID: Components.ID("{c6cf88b7-452e-47eb-bdc9-86e3561648ef}"), + QueryInterface: ChromeUtils.generateQI(["nsIURIFixup"]), +}; + +export function URIFixupInfo(originalInput = "") { + this._originalInput = originalInput; +} + +URIFixupInfo.prototype = { + set consumer(consumer) { + this._consumer = consumer || null; + }, + get consumer() { + return this._consumer || null; + }, + + set preferredURI(uri) { + this._preferredURI = uri; + }, + get preferredURI() { + return this._preferredURI || null; + }, + + set fixedURI(uri) { + this._fixedURI = uri; + }, + get fixedURI() { + return this._fixedURI || null; + }, + + set keywordProviderName(name) { + this._keywordProviderName = name; + }, + get keywordProviderName() { + return this._keywordProviderName || ""; + }, + + set keywordAsSent(keyword) { + this._keywordAsSent = keyword; + }, + get keywordAsSent() { + return this._keywordAsSent || ""; + }, + + set fixupChangedProtocol(changed) { + this._fixupChangedProtocol = changed; + }, + get fixupChangedProtocol() { + return !!this._fixupChangedProtocol; + }, + + set fixupCreatedAlternateURI(changed) { + this._fixupCreatedAlternateURI = changed; + }, + get fixupCreatedAlternateURI() { + return !!this._fixupCreatedAlternateURI; + }, + + set originalInput(input) { + this._originalInput = input; + }, + get originalInput() { + return this._originalInput || ""; + }, + + set postData(postData) { + this._postData = postData; + }, + get postData() { + return this._postData || null; + }, + + classID: Components.ID("{33d75835-722f-42c0-89cc-44f328e56a86}"), + QueryInterface: ChromeUtils.generateQI(["nsIURIFixupInfo"]), +}; + +// Helpers + +/** + * Implementation of isDomainKnown, so we don't have to go through the + * service. + * @param {string} asciiHost + * @returns {boolean} whether the domain is known + */ +function isDomainKnown(asciiHost) { + if (lazy.dnsFirstForSingleWords) { + return true; + } + // Check if this domain is known as an actual + // domain (which will prevent a keyword query) + // Note that any processing of the host here should stay in sync with + // code in the front-end(s) that set the pref. + let lastDotIndex = asciiHost.lastIndexOf("."); + if (lastDotIndex == asciiHost.length - 1) { + asciiHost = asciiHost.substring(0, asciiHost.length - 1); + lastDotIndex = asciiHost.lastIndexOf("."); + } + if (lazy.knownDomains.has(asciiHost.toLowerCase())) { + return true; + } + // If there's no dot or only a leading dot we are done, otherwise we'll check + // against the known suffixes. + if (lastDotIndex <= 0) { + return false; + } + // Don't use getPublicSuffix here, since the suffix is not in the PSL, + // thus it couldn't tell if the suffix is made up of one or multiple + // dot-separated parts. + let lastPart = asciiHost.substr(lastDotIndex + 1); + let suffixes = lazy.knownSuffixes.get(lastPart); + if (suffixes) { + return Array.from(suffixes).some(s => asciiHost.endsWith(s)); + } + return false; +} + +/** + * Checks the suffix of info.fixedURI against the Public Suffix List. + * If the suffix is unknown due to a typo this will try to fix it up. + * @param {URIFixupInfo} info about the uri to check. + * @note this may modify the public suffix of info.fixedURI. + * @returns {object} result The lookup result. + * @returns {string} result.suffix The public suffix if one can be identified. + * @returns {boolean} result.hasUnknownSuffix True when the suffix is not in the + * Public Suffix List and it's not in knownSuffixes. False in the other cases. + */ +function checkAndFixPublicSuffix(info) { + let uri = info.fixedURI; + let asciiHost = uri?.asciiHost; + if ( + !asciiHost || + !asciiHost.includes(".") || + asciiHost.endsWith(".") || + isDomainKnown(asciiHost) + ) { + return { suffix: "", hasUnknownSuffix: false }; + } + + // Quick bailouts for most common cases, according to Alexa Top 1 million. + if ( + /^\w/.test(asciiHost) && + (asciiHost.endsWith(".com") || + asciiHost.endsWith(".net") || + asciiHost.endsWith(".org") || + asciiHost.endsWith(".ru") || + asciiHost.endsWith(".de")) + ) { + return { + suffix: asciiHost.substring(asciiHost.lastIndexOf(".") + 1), + hasUnknownSuffix: false, + }; + } + try { + let suffix = Services.eTLD.getKnownPublicSuffix(uri); + if (suffix) { + return { suffix, hasUnknownSuffix: false }; + } + } catch (ex) { + return { suffix: "", hasUnknownSuffix: false }; + } + // Suffix is unknown, try to fix most common 3 chars TLDs typos. + // .com is the most commonly mistyped tld, so it has more cases. + let suffix = Services.eTLD.getPublicSuffix(uri); + if (!suffix || lazy.numberRegex.test(suffix)) { + return { suffix: "", hasUnknownSuffix: false }; + } + for (let [typo, fixed] of [ + ["ocm", "com"], + ["con", "com"], + ["cmo", "com"], + ["xom", "com"], + ["vom", "com"], + ["cpm", "com"], + ["com'", "com"], + ["ent", "net"], + ["ner", "net"], + ["nte", "net"], + ["met", "net"], + ["rog", "org"], + ["ogr", "org"], + ["prg", "org"], + ["orh", "org"], + ]) { + if (suffix == typo) { + let host = uri.host.substring(0, uri.host.length - typo.length) + fixed; + let updatePreferredURI = info.preferredURI == info.fixedURI; + info.fixedURI = uri.mutate().setHost(host).finalize(); + if (updatePreferredURI) { + info.preferredURI = info.fixedURI; + } + return { suffix: fixed, hasUnknownSuffix: false }; + } + } + return { suffix: "", hasUnknownSuffix: true }; +} + +function tryKeywordFixupForURIInfo(uriString, fixupInfo, isPrivateContext) { + try { + let keywordInfo = Services.uriFixup.keywordToURI( + uriString, + isPrivateContext + ); + fixupInfo.keywordProviderName = keywordInfo.keywordProviderName; + fixupInfo.keywordAsSent = keywordInfo.keywordAsSent; + fixupInfo.preferredURI = keywordInfo.preferredURI; + return true; + } catch (ex) {} + return false; +} + +/** + * This generates an alternate fixedURI, by adding a prefix and a suffix to + * the fixedURI host, if and only if the protocol is http. It should _never_ + * modify URIs with other protocols. + * @param {URIFixupInfo} info an URIInfo object + * @param {integer} fixupFlags the fixup flags + * @returns {boolean} Whether an alternate uri was generated + */ +function maybeSetAlternateFixedURI(info, fixupFlags) { + let uri = info.fixedURI; + let canUseAlternate = + fixupFlags & FIXUP_FLAG_FORCE_ALTERNATE_URI || + (lazy.alternateEnabled && fixupFlags & FIXUP_FLAGS_MAKE_ALTERNATE_URI); + if ( + !canUseAlternate || + // Code only works for http. Not for any other protocol including https! + !uri.schemeIs("http") || + // Security - URLs with user / password info should NOT be fixed up + uri.userPass || + // Don't fix up hosts with ports + uri.port != -1 + ) { + return false; + } + + let oldHost = uri.host; + // Don't create an alternate uri for localhost, because it would be confusing. + // Ditto for 'http' and 'https' as these are frequently the result of typos, e.g. + // 'https//foo' (note missing : ). + if (oldHost == "localhost" || oldHost == "http" || oldHost == "https") { + return false; + } + + // Get the prefix and suffix to stick onto the new hostname. By default these + // are www. & .com but they could be any other value, e.g. www. & .org + let newHost = maybeAddPrefixAndSuffix(oldHost); + + if (newHost == oldHost) { + return false; + } + + return updateHostAndScheme(info, newHost); +} + +/** + * Try to fixup a file URI. + * @param {string} uriString The file URI to fix. + * @returns {nsIURI} a fixed uri or null. + * @note FileURIFixup only returns a URI if it has to add the file: protocol. + */ +function fileURIFixup(uriString) { + let attemptFixup = false; + let path = uriString; + if (AppConstants.platform == "win") { + // Check for "\"" in the url-string, just a drive (e.g. C:), + // or 'A:/...' where the "protocol" is also a single letter. + attemptFixup = + uriString.includes("\\") || + (uriString[1] == ":" && (uriString.length == 2 || uriString[2] == "/")); + if (uriString[1] == ":" && uriString[2] == "/") { + path = uriString.replace(/\//g, "\\"); + } + } else { + // UNIX: Check if it starts with "/". + attemptFixup = uriString.startsWith("/"); + } + if (attemptFixup) { + try { + // Test if this is a valid path by trying to create a local file + // object. The URL of that is returned if successful. + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(path); + return Services.io.newURI( + lazy.fileProtocolHandler.getURLSpecFromActualFile(file) + ); + } catch (ex) { + // Not a file uri. + } + } + return null; +} + +/** + * Tries to fixup a string to an nsIURI by adding the default protocol. + * + * Should fix things like: + * no-scheme.com + * ftp.no-scheme.com + * ftp4.no-scheme.com + * no-scheme.com/query?foo=http://www.foo.com + * user:pass@no-scheme.com + * + * @param {string} uriString The string to fixup. + * @returns {nsIURI} an nsIURI built adding the default protocol to the string, + * or null if fixing was not possible. + */ +function fixupURIProtocol(uriString) { + let schemePos = uriString.indexOf("://"); + if (schemePos == -1 || schemePos > uriString.search(/[:\/]/)) { + uriString = "http://" + uriString; + } + try { + return Services.io.newURI(uriString); + } catch (ex) { + // We generated an invalid uri. + } + return null; +} + +/** + * Tries to fixup a string to a search url. + * @param {string} uriString the string to fixup. + * @param {URIFixupInfo} fixupInfo The fixup info object, modified in-place. + * @param {boolean} isPrivateContext Whether this happens in a private context. + * @param {nsIInputStream} postData optional POST data for the search + * @returns {boolean} Whether the keyword fixup was succesful. + */ +function keywordURIFixup(uriString, fixupInfo, isPrivateContext) { + // Here is a few examples of strings that should be searched: + // "what is mozilla" + // "what is mozilla?" + // "docshell site:mozilla.org" - has a space in the origin part + // "?site:mozilla.org - anything that begins with a question mark + // "mozilla'.org" - Things that have a quote before the first dot/colon + // "mozilla/test" - unknown host + // ".mozilla", "mozilla." - starts or ends with a dot () + // "user@nonQualifiedHost" + + // These other strings should not be searched, because they could be URIs: + // "www.blah.com" - Domain with a standard or known suffix + // "knowndomain" - known domain + // "nonQualifiedHost:8888?something" - has a port + // "user:pass@nonQualifiedHost" + // "blah.com." + + // We do keyword lookups if the input starts with a question mark. + if (uriString.startsWith("?")) { + return tryKeywordFixupForURIInfo( + fixupInfo.originalInput, + fixupInfo, + isPrivateContext + ); + } + + // Check for IPs. + const userPassword = lazy.userPasswordRegex.exec(uriString); + const ipString = userPassword + ? uriString.replace(userPassword[2], "") + : uriString; + if (lazy.IPv4LikeRegex.test(ipString) || lazy.IPv6LikeRegex.test(ipString)) { + return false; + } + + // Avoid keyword lookup if we can identify a host and it's known, or ends + // with a dot and has some path. + // Note that if dnsFirstForSingleWords is true isDomainKnown will always + // return true, so we can avoid checking dnsFirstForSingleWords after this. + let asciiHost = fixupInfo.fixedURI?.asciiHost; + if ( + asciiHost && + (isDomainKnown(asciiHost) || + (asciiHost.endsWith(".") && + asciiHost.indexOf(".") != asciiHost.length - 1)) + ) { + return false; + } + + // Avoid keyword lookup if the url seems to have password. + if (fixupInfo.fixedURI?.password) { + return false; + } + + // Even if the host is unknown, avoid keyword lookup if the string has + // uri-like characteristics, unless it looks like "user@unknownHost". + // Note we already excluded passwords at this point. + if ( + !isURILike(uriString, fixupInfo.fixedURI?.displayHost) || + (fixupInfo.fixedURI?.userPass && fixupInfo.fixedURI?.pathQueryRef === "/") + ) { + return tryKeywordFixupForURIInfo( + fixupInfo.originalInput, + fixupInfo, + isPrivateContext + ); + } + + return false; +} + +/** + * Mimics the logic in Services.io.extractScheme, but avoids crossing XPConnect. + * This also tries to fixup the scheme if it was clearly mistyped. + * @param {string} uriString the string to examine + * @param {integer} fixupFlags The original fixup flags + * @returns {object} + * scheme: a typo fixed scheme or empty string if one could not be identified + * fixedSchemeUriString: uri string with a typo fixed scheme + * fixupChangedProtocol: true if the scheme is fixed up + */ +function extractScheme(uriString, fixupFlags = FIXUP_FLAG_NONE) { + const matches = uriString.match(lazy.possibleProtocolRegex); + const hasColon = matches?.[2] === ":"; + const hasSlash2 = matches?.[3] === "//"; + + const isFixupSchemeTypos = + lazy.fixupSchemeTypos && fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS; + + if ( + !matches || + (!hasColon && !hasSlash2) || + (!hasColon && !isFixupSchemeTypos) + ) { + return { + scheme: "", + fixedSchemeUriString: uriString, + fixupChangedProtocol: false, + }; + } + + let scheme = matches[1].replace("\t", "").toLowerCase(); + let fixedSchemeUriString = uriString; + + if (isFixupSchemeTypos && hasSlash2) { + // Fix up typos for string that user would have intented as protocol. + const afterProtocol = uriString.substring(matches[0].length); + fixedSchemeUriString = `${scheme}://${afterProtocol}`; + } + + let fixupChangedProtocol = false; + + if (isFixupSchemeTypos) { + // Fix up common scheme typos. + // TODO: Use levenshtein distance here? + fixupChangedProtocol = [ + ["ttp", "http"], + ["htp", "http"], + ["ttps", "https"], + ["tps", "https"], + ["ps", "https"], + ["htps", "https"], + ["ile", "file"], + ["le", "file"], + ].some(([typo, fixed]) => { + if (scheme === typo) { + scheme = fixed; + fixedSchemeUriString = + scheme + fixedSchemeUriString.substring(typo.length); + return true; + } + return false; + }); + } + + return { + scheme, + fixedSchemeUriString, + fixupChangedProtocol, + }; +} + +/** + * View-source is a pseudo scheme. We're interested in fixing up the stuff + * after it. The easiest way to do that is to call this method again with + * the "view-source:" lopped off and then prepend it again afterwards. + * @param {string} uriString The original string to fixup + * @param {integer} fixupFlags The original fixup flags + * @param {nsIInputStream} postData Optional POST data for the search + * @returns {object} {preferredURI, postData} The fixed URI and relative postData + * @throws if it's not possible to fixup the url + */ +function fixupViewSource(uriString, fixupFlags) { + // We disable keyword lookup and alternate URIs so that small typos don't + // cause us to look at very different domains. + let newFixupFlags = + fixupFlags & + ~FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP & + ~FIXUP_FLAGS_MAKE_ALTERNATE_URI; + + let innerURIString = uriString.substring(12).trim(); + + // Prevent recursion. + const { scheme: innerScheme } = extractScheme(innerURIString); + if (innerScheme == "view-source") { + throw new Components.Exception( + "Prevent view-source recursion", + Cr.NS_ERROR_FAILURE + ); + } + + let info = Services.uriFixup.getFixupURIInfo(innerURIString, newFixupFlags); + if (!info.preferredURI) { + throw new Components.Exception( + "Couldn't build a valid uri", + Cr.NS_ERROR_MALFORMED_URI + ); + } + return { + preferredURI: Services.io.newURI("view-source:" + info.preferredURI.spec), + postData: info.postData, + }; +} + +/** + * Fixup the host of fixedURI if it contains consecutive dots. + * @param {URIFixupInfo} info an URIInfo object + */ +function fixupConsecutiveDotsHost(fixupInfo) { + const uri = fixupInfo.fixedURI; + + try { + if (!uri?.host.includes("..")) { + return; + } + } catch (e) { + return; + } + + try { + const isPreferredEqualsToFixed = fixupInfo.preferredURI?.equals(uri); + + fixupInfo.fixedURI = uri + .mutate() + .setHost(uri.host.replace(/\.+/g, ".")) + .finalize(); + + if (isPreferredEqualsToFixed) { + fixupInfo.preferredURI = fixupInfo.fixedURI; + } + } catch (e) { + if (e.result !== Cr.NS_ERROR_MALFORMED_URI) { + throw e; + } + } +} + +/** + * Return whether or not given string is uri like. + * This function returns true like following strings. + * - ":8080" + * - "localhost:8080" (if given host is "localhost") + * - "/foo?bar" + * - "/foo#bar" + * @param {string} uriString. + * @param {string} host. + * @param {boolean} true if uri like. + */ +function isURILike(uriString, host) { + const indexOfSlash = uriString.indexOf("/"); + if ( + indexOfSlash >= 0 && + (indexOfSlash < uriString.indexOf("?", indexOfSlash) || + indexOfSlash < uriString.indexOf("#", indexOfSlash)) + ) { + return true; + } + + if (uriString.startsWith(host)) { + uriString = uriString.substring(host.length); + } + + return lazy.portRegex.test(uriString); +} + +/** + * Add prefix and suffix to a hostname if both are missing. + * + * If the host does not start with the prefix, add the prefix to + * the hostname. + * + * By default the prefix and suffix are www. and .com but they could + * be any value e.g. www. and .org as they use the preferences + * "browser.fixup.alternate.prefix" and "browser.fixup.alternative.suffix" + * + * If no changes were made, it returns an empty string. + * + * @param {string} oldHost. + * @return {String} Fixed up hostname or an empty string. + */ +function maybeAddPrefixAndSuffix(oldHost) { + let prefix = Services.prefs.getCharPref( + "browser.fixup.alternate.prefix", + "www." + ); + let suffix = Services.prefs.getCharPref( + "browser.fixup.alternate.suffix", + ".com" + ); + let newHost = ""; + let numDots = (oldHost.match(/\./g) || []).length; + if (numDots == 0) { + newHost = prefix + oldHost + suffix; + } else if (numDots == 1) { + if (prefix && oldHost == prefix) { + newHost = oldHost + suffix; + } else if (suffix && !oldHost.startsWith(prefix)) { + newHost = prefix + oldHost; + } + } + return newHost ? newHost : oldHost; +} + +/** + * Given an instance of URIFixupInfo, update its fixedURI. + * + * First, change the protocol to the one stored in + * "browser.fixup.alternate.protocol". + * + * Then, try to update fixedURI's host to newHost. + * + * @param {URIFixupInfo} info. + * @param {string} newHost. + * @return {boolean} + * True, if info was updated without any errors. + * False, if NS_ERROR_MALFORMED_URI error. + * @throws If a non-NS_ERROR_MALFORMED_URI error occurs. + */ +function updateHostAndScheme(info, newHost) { + let oldHost = info.fixedURI.host; + let oldScheme = info.fixedURI.scheme; + try { + info.fixedURI = info.fixedURI + .mutate() + .setScheme(lazy.alternateProtocol) + .setHost(newHost) + .finalize(); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_MALFORMED_URI) { + throw ex; + } + return false; + } + if (oldScheme != info.fixedURI.scheme) { + info.fixupChangedProtocol = true; + } + if (oldHost != info.fixedURI.host) { + info.fixupCreatedAlternateURI = true; + } + return true; +} diff --git a/docshell/base/WindowContext.cpp b/docshell/base/WindowContext.cpp new file mode 100644 index 0000000000..02b4e7d4a8 --- /dev/null +++ b/docshell/base/WindowContext.cpp @@ -0,0 +1,662 @@ +/* -*- 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/. */ + +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/WindowGlobalActorsBinding.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/SyncedContextInlines.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/UserActivationIPCUtils.h" +#include "mozilla/PermissionDelegateIPCUtils.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/ClearOnShutdown.h" +#include "nsGlobalWindowInner.h" +#include "nsIScriptError.h" +#include "nsIXULRuntime.h" +#include "nsRefPtrHashtable.h" +#include "nsContentUtils.h" + +namespace mozilla { +namespace dom { + +// Explicit specialization of the `Transaction` type. Required by the `extern +// template class` declaration in the header. +template class syncedcontext::Transaction<WindowContext>; + +static LazyLogModule gWindowContextLog("WindowContext"); +static LazyLogModule gWindowContextSyncLog("WindowContextSync"); + +extern mozilla::LazyLogModule gUserInteractionPRLog; + +#define USER_ACTIVATION_LOG(msg, ...) \ + MOZ_LOG(gUserInteractionPRLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +using WindowContextByIdMap = nsTHashMap<nsUint64HashKey, WindowContext*>; +static StaticAutoPtr<WindowContextByIdMap> gWindowContexts; + +/* static */ +LogModule* WindowContext::GetLog() { return gWindowContextLog; } + +/* static */ +LogModule* WindowContext::GetSyncLog() { return gWindowContextSyncLog; } + +/* static */ +already_AddRefed<WindowContext> WindowContext::GetById( + uint64_t aInnerWindowId) { + if (!gWindowContexts) { + return nullptr; + } + return do_AddRef(gWindowContexts->Get(aInnerWindowId)); +} + +BrowsingContextGroup* WindowContext::Group() const { + return mBrowsingContext->Group(); +} + +WindowGlobalParent* WindowContext::Canonical() { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + return static_cast<WindowGlobalParent*>(this); +} + +bool WindowContext::IsCurrent() const { + return mBrowsingContext->mCurrentWindowContext == this; +} + +bool WindowContext::IsInBFCache() { + if (mozilla::SessionHistoryInParent()) { + return mBrowsingContext->IsInBFCache(); + } + return TopWindowContext()->GetWindowStateSaved(); +} + +nsGlobalWindowInner* WindowContext::GetInnerWindow() const { + return mWindowGlobalChild ? mWindowGlobalChild->GetWindowGlobal() : nullptr; +} + +Document* WindowContext::GetDocument() const { + nsGlobalWindowInner* innerWindow = GetInnerWindow(); + return innerWindow ? innerWindow->GetDocument() : nullptr; +} + +Document* WindowContext::GetExtantDoc() const { + nsGlobalWindowInner* innerWindow = GetInnerWindow(); + return innerWindow ? innerWindow->GetExtantDoc() : nullptr; +} + +WindowGlobalChild* WindowContext::GetWindowGlobalChild() const { + return mWindowGlobalChild; +} + +WindowContext* WindowContext::GetParentWindowContext() { + return mBrowsingContext->GetParentWindowContext(); +} + +WindowContext* WindowContext::TopWindowContext() { + WindowContext* current = this; + while (current->GetParentWindowContext()) { + current = current->GetParentWindowContext(); + } + return current; +} + +bool WindowContext::IsTop() const { return mBrowsingContext->IsTop(); } + +bool WindowContext::SameOriginWithTop() const { + return mBrowsingContext->SameOriginWithTop(); +} + +nsIGlobalObject* WindowContext::GetParentObject() const { + return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); +} + +void WindowContext::AppendChildBrowsingContext( + BrowsingContext* aBrowsingContext) { + MOZ_DIAGNOSTIC_ASSERT(Group() == aBrowsingContext->Group(), + "Mismatched groups?"); + MOZ_DIAGNOSTIC_ASSERT(!mChildren.Contains(aBrowsingContext)); + + mChildren.AppendElement(aBrowsingContext); + if (!nsContentUtils::ShouldHideObjectOrEmbedImageDocument() || + !aBrowsingContext->IsEmbedderTypeObjectOrEmbed()) { + mNonSyntheticChildren.AppendElement(aBrowsingContext); + } + + // If we're the current WindowContext in our BrowsingContext, make sure to + // clear any cached `children` value. + if (IsCurrent()) { + BrowsingContext_Binding::ClearCachedChildrenValue(mBrowsingContext); + } +} + +void WindowContext::RemoveChildBrowsingContext( + BrowsingContext* aBrowsingContext) { + MOZ_DIAGNOSTIC_ASSERT(Group() == aBrowsingContext->Group(), + "Mismatched groups?"); + + mChildren.RemoveElement(aBrowsingContext); + mNonSyntheticChildren.RemoveElement(aBrowsingContext); + + // If we're the current WindowContext in our BrowsingContext, make sure to + // clear any cached `children` value. + if (IsCurrent()) { + BrowsingContext_Binding::ClearCachedChildrenValue(mBrowsingContext); + } +} + +void WindowContext::UpdateChildSynthetic(BrowsingContext* aBrowsingContext, + bool aIsSynthetic) { + MOZ_ASSERT(nsContentUtils::ShouldHideObjectOrEmbedImageDocument()); + if (aIsSynthetic) { + mNonSyntheticChildren.RemoveElement(aBrowsingContext); + } else { + // The same BrowsingContext will be reused for error pages, so it can be in + // the list already. + if (!mNonSyntheticChildren.Contains(aBrowsingContext)) { + mNonSyntheticChildren.AppendElement(aBrowsingContext); + } + } +} + +void WindowContext::SendCommitTransaction(ContentParent* aParent, + const BaseTransaction& aTxn, + uint64_t aEpoch) { + Unused << aParent->SendCommitWindowContextTransaction(this, aTxn, aEpoch); +} + +void WindowContext::SendCommitTransaction(ContentChild* aChild, + const BaseTransaction& aTxn, + uint64_t aEpoch) { + aChild->SendCommitWindowContextTransaction(this, aTxn, aEpoch); +} + +bool WindowContext::CheckOnlyOwningProcessCanSet(ContentParent* aSource) { + if (IsInProcess()) { + return true; + } + + if (XRE_IsParentProcess() && aSource) { + return Canonical()->GetContentParent() == aSource; + } + + return false; +} + +bool WindowContext::CanSet(FieldIndex<IDX_IsSecure>, const bool& aIsSecure, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_AllowMixedContent>, + const bool& aAllowMixedContent, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_HasBeforeUnload>, + const bool& aHasBeforeUnload, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_CookieBehavior>, + const Maybe<uint32_t>& aValue, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_IsOnContentBlockingAllowList>, + const bool& aValue, ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_IsThirdPartyWindow>, + const bool& IsThirdPartyWindow, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_IsThirdPartyTrackingResourceWindow>, + const bool& aIsThirdPartyTrackingResourceWindow, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_ShouldResistFingerprinting>, + const bool& aShouldResistFingerprinting, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_IsSecureContext>, + const bool& aIsSecureContext, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_IsOriginalFrameSource>, + const bool& aIsOriginalFrameSource, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_DocTreeHadMedia>, const bool& aValue, + ContentParent* aSource) { + return IsTop(); +} + +bool WindowContext::CanSet(FieldIndex<IDX_AutoplayPermission>, + const uint32_t& aValue, ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_ShortcutsPermission>, + const uint32_t& aValue, ContentParent* aSource) { + return IsTop() && CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_ActiveMediaSessionContextId>, + const Maybe<uint64_t>& aValue, + ContentParent* aSource) { + return IsTop(); +} + +bool WindowContext::CanSet(FieldIndex<IDX_PopupPermission>, const uint32_t&, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet( + FieldIndex<IDX_DelegatedPermissions>, + const PermissionDelegateHandler::DelegatedPermissionList& aValue, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet( + FieldIndex<IDX_DelegatedExactHostMatchPermissions>, + const PermissionDelegateHandler::DelegatedPermissionList& aValue, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_IsLocalIP>, const bool& aValue, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue, + ContentParent* aSource) { + return (XRE_IsParentProcess() && !aSource) || + CheckOnlyOwningProcessCanSet(aSource); +} + +void WindowContext::DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue) { + RecomputeCanExecuteScripts(); +} + +bool WindowContext::CanSet(FieldIndex<IDX_HasActivePeerConnections>, bool, + ContentParent*) { + return XRE_IsParentProcess() && IsTop(); +} + +void WindowContext::RecomputeCanExecuteScripts(bool aApplyChanges) { + const bool old = mCanExecuteScripts; + if (!AllowJavascript()) { + // Scripting has been explicitly disabled on our WindowContext. + mCanExecuteScripts = false; + } else { + // Otherwise, inherit. + mCanExecuteScripts = mBrowsingContext->CanExecuteScripts(); + } + + if (aApplyChanges && old != mCanExecuteScripts) { + // Inform our active DOM window. + if (nsGlobalWindowInner* window = GetInnerWindow()) { + // Only update scriptability if the window is current. Windows will have + // scriptability disabled when entering the bfcache and updated when + // coming out. + if (window->IsCurrentInnerWindow()) { + auto& scriptability = + xpc::Scriptability::Get(window->GetGlobalJSObject()); + scriptability.SetWindowAllowsScript(mCanExecuteScripts); + } + } + + for (const RefPtr<BrowsingContext>& child : Children()) { + child->RecomputeCanExecuteScripts(); + } + } +} + +void WindowContext::DidSet(FieldIndex<IDX_SHEntryHasUserInteraction>, + bool aOldValue) { + MOZ_ASSERT( + TopWindowContext() == this, + "SHEntryHasUserInteraction can only be set on the top window context"); + // This field is set when the child notifies us of new user interaction, so we + // also set the currently active shentry in the parent as having interaction. + if (XRE_IsParentProcess() && mBrowsingContext) { + SessionHistoryEntry* activeEntry = + mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry(); + if (activeEntry && GetSHEntryHasUserInteraction()) { + activeEntry->SetHasUserInteraction(true); + } + } +} + +void WindowContext::DidSet(FieldIndex<IDX_UserActivationState>) { + MOZ_ASSERT_IF(!IsInProcess(), mUserGestureStart.IsNull()); + USER_ACTIVATION_LOG("Set user gesture activation %" PRIu8 + " for %s browsing context 0x%08" PRIx64, + static_cast<uint8_t>(GetUserActivationState()), + XRE_IsParentProcess() ? "Parent" : "Child", Id()); + if (IsInProcess()) { + USER_ACTIVATION_LOG( + "Set user gesture start time for %s browsing context 0x%08" PRIx64, + XRE_IsParentProcess() ? "Parent" : "Child", Id()); + if (GetUserActivationState() == UserActivation::State::FullActivated) { + mUserGestureStart = TimeStamp::Now(); + } else if (GetUserActivationState() == UserActivation::State::None) { + mUserGestureStart = TimeStamp(); + } + } +} + +void WindowContext::DidSet(FieldIndex<IDX_HasReportedShadowDOMUsage>, + bool aOldValue) { + if (!aOldValue && GetHasReportedShadowDOMUsage() && IsInProcess()) { + MOZ_ASSERT(TopWindowContext() == this); + if (mBrowsingContext) { + Document* topLevelDoc = mBrowsingContext->GetDocument(); + if (topLevelDoc) { + nsAutoString uri; + Unused << topLevelDoc->GetDocumentURI(uri); + if (!uri.IsEmpty()) { + nsAutoString msg = u"Shadow DOM used in ["_ns + uri + + u"] or in some of its subdocuments."_ns; + nsContentUtils::ReportToConsoleNonLocalized( + msg, nsIScriptError::infoFlag, "DOM"_ns, topLevelDoc); + } + } + } + } +} + +bool WindowContext::CanSet(FieldIndex<IDX_WindowStateSaved>, bool aValue, + ContentParent* aSource) { + return !mozilla::SessionHistoryInParent() && IsTop() && + CheckOnlyOwningProcessCanSet(aSource); +} + +void WindowContext::CreateFromIPC(IPCInitializer&& aInit) { + MOZ_RELEASE_ASSERT(XRE_IsContentProcess(), + "Should be a WindowGlobalParent in the parent"); + + RefPtr<BrowsingContext> bc = BrowsingContext::Get(aInit.mBrowsingContextId); + MOZ_RELEASE_ASSERT(bc); + + if (bc->IsDiscarded()) { + // If we have already closed our browsing context, the + // WindowGlobalChild actor is bound to be destroyed soon and it's + // safe to ignore creating the WindowContext. + return; + } + + RefPtr<WindowContext> context = new WindowContext( + bc, aInit.mInnerWindowId, aInit.mOuterWindowId, std::move(aInit.mFields)); + context->Init(); +} + +void WindowContext::Init() { + MOZ_LOG(GetLog(), LogLevel::Debug, + ("Registering 0x%" PRIx64 " (bc=0x%" PRIx64 ")", mInnerWindowId, + mBrowsingContext->Id())); + + // Register the WindowContext in the `WindowContextByIdMap`. + if (!gWindowContexts) { + gWindowContexts = new WindowContextByIdMap(); + ClearOnShutdown(&gWindowContexts); + } + auto& entry = gWindowContexts->LookupOrInsert(mInnerWindowId); + MOZ_RELEASE_ASSERT(!entry, "Duplicate WindowContext for ID!"); + entry = this; + + // Register this to the browsing context. + mBrowsingContext->RegisterWindowContext(this); + Group()->Register(this); +} + +void WindowContext::Discard() { + MOZ_LOG(GetLog(), LogLevel::Debug, + ("Discarding 0x%" PRIx64 " (bc=0x%" PRIx64 ")", mInnerWindowId, + mBrowsingContext->Id())); + if (mIsDiscarded) { + return; + } + + mIsDiscarded = true; + if (gWindowContexts) { + gWindowContexts->Remove(InnerWindowId()); + } + mBrowsingContext->UnregisterWindowContext(this); + Group()->Unregister(this); +} + +void WindowContext::AddSecurityState(uint32_t aStateFlags) { + MOZ_ASSERT(TopWindowContext() == this); + MOZ_ASSERT((aStateFlags & + (nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT | + nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT | + nsIWebProgressListener::STATE_BLOCKED_MIXED_DISPLAY_CONTENT | + nsIWebProgressListener::STATE_BLOCKED_MIXED_ACTIVE_CONTENT | + nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADED | + nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED)) == + aStateFlags, + "Invalid flags specified!"); + + if (XRE_IsParentProcess()) { + Canonical()->AddSecurityState(aStateFlags); + } else { + ContentChild* child = ContentChild::GetSingleton(); + child->SendAddSecurityState(this, aStateFlags); + } +} + +void WindowContext::NotifyUserGestureActivation() { + Unused << SetUserActivationState(UserActivation::State::FullActivated); +} + +void WindowContext::NotifyResetUserGestureActivation() { + Unused << SetUserActivationState(UserActivation::State::None); +} + +bool WindowContext::HasBeenUserGestureActivated() { + return GetUserActivationState() != UserActivation::State::None; +} + +const TimeStamp& WindowContext::GetUserGestureStart() const { + MOZ_ASSERT(IsInProcess()); + return mUserGestureStart; +} + +bool WindowContext::HasValidTransientUserGestureActivation() { + MOZ_ASSERT(IsInProcess()); + + if (GetUserActivationState() != UserActivation::State::FullActivated) { + // mUserGestureStart should be null if the document hasn't ever been + // activated by user gesture + MOZ_ASSERT_IF(GetUserActivationState() == UserActivation::State::None, + mUserGestureStart.IsNull()); + return false; + } + + MOZ_ASSERT(!mUserGestureStart.IsNull(), + "mUserGestureStart shouldn't be null if the document has ever " + "been activated by user gesture"); + TimeDuration timeout = TimeDuration::FromMilliseconds( + StaticPrefs::dom_user_activation_transient_timeout()); + + return timeout <= TimeDuration() || + (TimeStamp::Now() - mUserGestureStart) <= timeout; +} + +bool WindowContext::ConsumeTransientUserGestureActivation() { + MOZ_ASSERT(IsInProcess()); + MOZ_ASSERT(IsCurrent()); + + if (!HasValidTransientUserGestureActivation()) { + return false; + } + + BrowsingContext* top = mBrowsingContext->Top(); + top->PreOrderWalk([&](BrowsingContext* aBrowsingContext) { + WindowContext* windowContext = aBrowsingContext->GetCurrentWindowContext(); + if (windowContext && windowContext->GetUserActivationState() == + UserActivation::State::FullActivated) { + Unused << windowContext->SetUserActivationState( + UserActivation::State::HasBeenActivated); + } + }); + + return true; +} + +bool WindowContext::CanShowPopup() { + uint32_t permit = GetPopupPermission(); + if (permit == nsIPermissionManager::ALLOW_ACTION) { + return true; + } + if (permit == nsIPermissionManager::DENY_ACTION) { + return false; + } + + return !StaticPrefs::dom_disable_open_during_load(); +} + +void WindowContext::TransientSetHasActivePeerConnections() { + if (!IsTop()) { + return; + } + + mFields.SetWithoutSyncing<IDX_HasActivePeerConnections>(true); +} + +WindowContext::IPCInitializer WindowContext::GetIPCInitializer() { + IPCInitializer init; + init.mInnerWindowId = mInnerWindowId; + init.mOuterWindowId = mOuterWindowId; + init.mBrowsingContextId = mBrowsingContext->Id(); + init.mFields = mFields.RawValues(); + return init; +} + +WindowContext::WindowContext(BrowsingContext* aBrowsingContext, + uint64_t aInnerWindowId, uint64_t aOuterWindowId, + FieldValues&& aInit) + : mFields(std::move(aInit)), + mInnerWindowId(aInnerWindowId), + mOuterWindowId(aOuterWindowId), + mBrowsingContext(aBrowsingContext) { + MOZ_ASSERT(mBrowsingContext); + MOZ_ASSERT(mInnerWindowId); + MOZ_ASSERT(mOuterWindowId); + RecomputeCanExecuteScripts(/* aApplyChanges */ false); +} + +WindowContext::~WindowContext() { + if (gWindowContexts) { + gWindowContexts->Remove(InnerWindowId()); + } +} + +JSObject* WindowContext::WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) { + return WindowContext_Binding::Wrap(cx, this, aGivenProto); +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WindowContext) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WindowContext) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WindowContext) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WindowContext) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WindowContext) + if (gWindowContexts) { + gWindowContexts->Remove(tmp->InnerWindowId()); + } + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBrowsingContext) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildren) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNonSyntheticChildren) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WindowContext) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBrowsingContext) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildren) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNonSyntheticChildren) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +} // namespace dom + +namespace ipc { + +void IPDLParamTraits<dom::MaybeDiscarded<dom::WindowContext>>::Write( + IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::MaybeDiscarded<dom::WindowContext>& aParam) { + uint64_t id = aParam.ContextId(); + WriteIPDLParam(aWriter, aActor, id); +} + +bool IPDLParamTraits<dom::MaybeDiscarded<dom::WindowContext>>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + dom::MaybeDiscarded<dom::WindowContext>* aResult) { + uint64_t id = 0; + if (!ReadIPDLParam(aReader, aActor, &id)) { + return false; + } + + if (id == 0) { + *aResult = nullptr; + } else if (RefPtr<dom::WindowContext> wc = dom::WindowContext::GetById(id)) { + *aResult = std::move(wc); + } else { + aResult->SetDiscarded(id); + } + return true; +} + +void IPDLParamTraits<dom::WindowContext::IPCInitializer>::Write( + IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::WindowContext::IPCInitializer& aInit) { + // Write actor ID parameters. + WriteIPDLParam(aWriter, aActor, aInit.mInnerWindowId); + WriteIPDLParam(aWriter, aActor, aInit.mOuterWindowId); + WriteIPDLParam(aWriter, aActor, aInit.mBrowsingContextId); + WriteIPDLParam(aWriter, aActor, aInit.mFields); +} + +bool IPDLParamTraits<dom::WindowContext::IPCInitializer>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + dom::WindowContext::IPCInitializer* aInit) { + // Read actor ID parameters. + return ReadIPDLParam(aReader, aActor, &aInit->mInnerWindowId) && + ReadIPDLParam(aReader, aActor, &aInit->mOuterWindowId) && + ReadIPDLParam(aReader, aActor, &aInit->mBrowsingContextId) && + ReadIPDLParam(aReader, aActor, &aInit->mFields); +} + +template struct IPDLParamTraits<dom::WindowContext::BaseTransaction>; + +} // namespace ipc +} // namespace mozilla diff --git a/docshell/base/WindowContext.h b/docshell/base/WindowContext.h new file mode 100644 index 0000000000..9396ad9ed1 --- /dev/null +++ b/docshell/base/WindowContext.h @@ -0,0 +1,399 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_WindowContext_h +#define mozilla_dom_WindowContext_h + +#include "mozilla/PermissionDelegateHandler.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/Span.h" +#include "mozilla/dom/MaybeDiscarded.h" +#include "mozilla/dom/SyncedContext.h" +#include "mozilla/dom/UserActivation.h" +#include "nsDOMNavigationTiming.h" +#include "nsILoadInfo.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +class nsGlobalWindowInner; + +namespace mozilla { +class LogModule; + +namespace dom { + +class WindowGlobalChild; +class WindowGlobalParent; +class WindowGlobalInit; +class BrowsingContext; +class BrowsingContextGroup; + +#define MOZ_EACH_WC_FIELD(FIELD) \ + /* Whether the SHEntry associated with the current top-level \ + * window has already seen user interaction. \ + * As such, this will be reset to false when a new SHEntry is \ + * created without changing the WC (e.g. when using pushState or \ + * sub-frame navigation) \ + * This flag is set for optimization purposes, to avoid \ + * having to get the top SHEntry and update it on every \ + * user interaction. \ + * This is only meaningful on the top-level WC. */ \ + FIELD(SHEntryHasUserInteraction, bool) \ + FIELD(CookieBehavior, Maybe<uint32_t>) \ + FIELD(IsOnContentBlockingAllowList, bool) \ + /* Whether the given window hierarchy is third party. See \ + * ThirdPartyUtil::IsThirdPartyWindow for details */ \ + FIELD(IsThirdPartyWindow, bool) \ + /* Whether this window's channel has been marked as a third-party \ + * tracking resource */ \ + FIELD(IsThirdPartyTrackingResourceWindow, bool) \ + FIELD(ShouldResistFingerprinting, bool) \ + FIELD(IsSecureContext, bool) \ + FIELD(IsOriginalFrameSource, bool) \ + /* Mixed-Content: If the corresponding documentURI is https, \ + * then this flag is true. */ \ + FIELD(IsSecure, bool) \ + /* Whether the user has overriden the mixed content blocker to allow \ + * mixed content loads to happen */ \ + FIELD(AllowMixedContent, bool) \ + /* Whether this window has registered a "beforeunload" event \ + * handler */ \ + FIELD(HasBeforeUnload, bool) \ + /* Controls whether the WindowContext is currently considered to be \ + * activated by a gesture */ \ + FIELD(UserActivationState, UserActivation::State) \ + FIELD(EmbedderPolicy, nsILoadInfo::CrossOriginEmbedderPolicy) \ + /* True if this document tree contained at least a HTMLMediaElement. \ + * This should only be set on top level context. */ \ + FIELD(DocTreeHadMedia, bool) \ + FIELD(AutoplayPermission, uint32_t) \ + FIELD(ShortcutsPermission, uint32_t) \ + /* Store the Id of the browsing context where active media session \ + * exists on the top level window context */ \ + FIELD(ActiveMediaSessionContextId, Maybe<uint64_t>) \ + /* ALLOW_ACTION if it is allowed to open popups for the sub-tree \ + * starting and including the current WindowContext */ \ + FIELD(PopupPermission, uint32_t) \ + FIELD(DelegatedPermissions, \ + PermissionDelegateHandler::DelegatedPermissionList) \ + FIELD(DelegatedExactHostMatchPermissions, \ + PermissionDelegateHandler::DelegatedPermissionList) \ + FIELD(HasReportedShadowDOMUsage, bool) \ + /* Whether the principal of this window is for a local \ + * IP address */ \ + FIELD(IsLocalIP, bool) \ + /* Whether any of the windows in the subtree rooted at this window has \ + * active peer connections or not (only set on the top window). */ \ + FIELD(HasActivePeerConnections, bool) \ + /* Whether we can execute scripts in this WindowContext. Has no effect \ + * unless scripts are also allowed in the BrowsingContext. */ \ + FIELD(AllowJavascript, bool) \ + /* If this field is `true`, it means that this WindowContext's \ + * WindowState was saved to be stored in the legacy (non-SHIP) BFCache \ + * implementation. Always false for SHIP */ \ + FIELD(WindowStateSaved, bool) + +class WindowContext : public nsISupports, public nsWrapperCache { + MOZ_DECL_SYNCED_CONTEXT(WindowContext, MOZ_EACH_WC_FIELD) + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WindowContext) + + public: + static already_AddRefed<WindowContext> GetById(uint64_t aInnerWindowId); + static LogModule* GetLog(); + static LogModule* GetSyncLog(); + + BrowsingContext* GetBrowsingContext() const { return mBrowsingContext; } + BrowsingContextGroup* Group() const; + uint64_t Id() const { return InnerWindowId(); } + uint64_t InnerWindowId() const { return mInnerWindowId; } + uint64_t OuterWindowId() const { return mOuterWindowId; } + bool IsDiscarded() const { return mIsDiscarded; } + + // Returns `true` if this WindowContext is the current WindowContext in its + // BrowsingContext. + bool IsCurrent() const; + + // Returns `true` if this WindowContext is currently in the BFCache. + bool IsInBFCache(); + + bool IsInProcess() const { return mIsInProcess; } + + bool HasBeforeUnload() const { return GetHasBeforeUnload(); } + + bool IsLocalIP() const { return GetIsLocalIP(); } + + bool ShouldResistFingerprinting() const { + return GetShouldResistFingerprinting(); + } + + nsGlobalWindowInner* GetInnerWindow() const; + Document* GetDocument() const; + Document* GetExtantDoc() const; + + WindowGlobalChild* GetWindowGlobalChild() const; + + // Get the parent WindowContext of this WindowContext, taking the BFCache into + // account. This will not cross chrome/content <browser> boundaries. + WindowContext* GetParentWindowContext(); + WindowContext* TopWindowContext(); + + bool SameOriginWithTop() const; + + bool IsTop() const; + + Span<RefPtr<BrowsingContext>> Children() { return mChildren; } + + // The filtered version of `Children()`, which contains no browsing contexts + // for synthetic documents as created by object loading content. + Span<RefPtr<BrowsingContext>> NonSyntheticChildren() { + return mNonSyntheticChildren; + } + + // Cast this object to it's parent-process canonical form. + WindowGlobalParent* Canonical(); + + nsIGlobalObject* GetParentObject() const; + JSObject* WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) override; + + void Discard(); + + struct IPCInitializer { + uint64_t mInnerWindowId; + uint64_t mOuterWindowId; + uint64_t mBrowsingContextId; + + FieldValues mFields; + }; + IPCInitializer GetIPCInitializer(); + + static void CreateFromIPC(IPCInitializer&& aInit); + + // Add new security state flags. + // These should be some of the nsIWebProgressListener 'HTTPS_ONLY_MODE' or + // 'MIXED' state flags, and should only be called on the top window context. + void AddSecurityState(uint32_t aStateFlags); + + // This function would be called when its corresponding window is activated + // by user gesture. + void NotifyUserGestureActivation(); + + // This function would be called when we want to reset the user gesture + // activation flag. + void NotifyResetUserGestureActivation(); + + // Return true if its corresponding window has been activated by user + // gesture. + bool HasBeenUserGestureActivated(); + + // Return true if its corresponding window has transient user gesture + // activation and the transient user gesture activation haven't yet timed + // out. + bool HasValidTransientUserGestureActivation(); + + // See `mUserGestureStart`. + const TimeStamp& GetUserGestureStart() const; + + // Return true if the corresponding window has valid transient user gesture + // activation and the transient user gesture activation had been consumed + // successfully. + bool ConsumeTransientUserGestureActivation(); + + bool CanShowPopup(); + + bool AllowJavascript() const { return GetAllowJavascript(); } + bool CanExecuteScripts() const { return mCanExecuteScripts; } + + void TransientSetHasActivePeerConnections(); + + protected: + WindowContext(BrowsingContext* aBrowsingContext, uint64_t aInnerWindowId, + uint64_t aOuterWindowId, FieldValues&& aFields); + virtual ~WindowContext(); + + virtual void Init(); + + private: + friend class BrowsingContext; + friend class WindowGlobalChild; + friend class WindowGlobalActor; + + void AppendChildBrowsingContext(BrowsingContext* aBrowsingContext); + void RemoveChildBrowsingContext(BrowsingContext* aBrowsingContext); + + // Update non-synthetic children based on whether `aBrowsingContext` + // is synthetic or not. Regardless the synthetic of `aBrowsingContext`, it is + // kept in this WindowContext's all children list. + void UpdateChildSynthetic(BrowsingContext* aBrowsingContext, + bool aIsSynthetic); + + // Send a given `BaseTransaction` object to the correct remote. + void SendCommitTransaction(ContentParent* aParent, + const BaseTransaction& aTxn, uint64_t aEpoch); + void SendCommitTransaction(ContentChild* aChild, const BaseTransaction& aTxn, + uint64_t aEpoch); + + bool CheckOnlyOwningProcessCanSet(ContentParent* aSource); + + // Overload `CanSet` to get notifications for a particular field being set. + bool CanSet(FieldIndex<IDX_IsSecure>, const bool& aIsSecure, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_AllowMixedContent>, const bool& aAllowMixedContent, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_HasBeforeUnload>, const bool& aHasBeforeUnload, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_CookieBehavior>, const Maybe<uint32_t>& aValue, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_IsOnContentBlockingAllowList>, const bool& aValue, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_EmbedderPolicy>, const bool& aValue, + ContentParent* aSource) { + return true; + } + + bool CanSet(FieldIndex<IDX_IsThirdPartyWindow>, + const bool& IsThirdPartyWindow, ContentParent* aSource); + bool CanSet(FieldIndex<IDX_IsThirdPartyTrackingResourceWindow>, + const bool& aIsThirdPartyTrackingResourceWindow, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_ShouldResistFingerprinting>, + const bool& aShouldResistFingerprinting, ContentParent* aSource); + bool CanSet(FieldIndex<IDX_IsSecureContext>, const bool& aIsSecureContext, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_IsOriginalFrameSource>, + const bool& aIsOriginalFrameSource, ContentParent* aSource); + bool CanSet(FieldIndex<IDX_DocTreeHadMedia>, const bool& aValue, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_AutoplayPermission>, const uint32_t& aValue, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_ShortcutsPermission>, const uint32_t& aValue, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_ActiveMediaSessionContextId>, + const Maybe<uint64_t>& aValue, ContentParent* aSource); + bool CanSet(FieldIndex<IDX_PopupPermission>, const uint32_t&, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_SHEntryHasUserInteraction>, + const bool& aSHEntryHasUserInteraction, ContentParent* aSource) { + return true; + } + bool CanSet(FieldIndex<IDX_DelegatedPermissions>, + const PermissionDelegateHandler::DelegatedPermissionList& aValue, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_DelegatedExactHostMatchPermissions>, + const PermissionDelegateHandler::DelegatedPermissionList& aValue, + ContentParent* aSource); + bool CanSet(FieldIndex<IDX_UserActivationState>, + const UserActivation::State& aUserActivationState, + ContentParent* aSource) { + return true; + } + + bool CanSet(FieldIndex<IDX_HasReportedShadowDOMUsage>, const bool& aValue, + ContentParent* aSource) { + return true; + } + + bool CanSet(FieldIndex<IDX_IsLocalIP>, const bool& aValue, + ContentParent* aSource); + + bool CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue, + ContentParent* aSource); + void DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue); + + bool CanSet(FieldIndex<IDX_HasActivePeerConnections>, bool, ContentParent*); + + void DidSet(FieldIndex<IDX_HasReportedShadowDOMUsage>, bool aOldValue); + + void DidSet(FieldIndex<IDX_SHEntryHasUserInteraction>, bool aOldValue); + + bool CanSet(FieldIndex<IDX_WindowStateSaved>, bool aValue, + ContentParent* aSource); + + // Overload `DidSet` to get notifications for a particular field being set. + // + // You can also overload the variant that gets the old value if you need it. + template <size_t I> + void DidSet(FieldIndex<I>) {} + template <size_t I, typename T> + void DidSet(FieldIndex<I>, T&& aOldValue) {} + void DidSet(FieldIndex<IDX_UserActivationState>); + + // Recomputes whether we can execute scripts in this WindowContext based on + // the value of AllowJavascript() and whether scripts are allowed in the + // BrowsingContext. + void RecomputeCanExecuteScripts(bool aApplyChanges = true); + + const uint64_t mInnerWindowId; + const uint64_t mOuterWindowId; + RefPtr<BrowsingContext> mBrowsingContext; + WeakPtr<WindowGlobalChild> mWindowGlobalChild; + + // --- NEVER CHANGE `mChildren` DIRECTLY! --- + // Changes to this list need to be synchronized to the list within our + // `mBrowsingContext`, and should only be performed through the + // `AppendChildBrowsingContext` and `RemoveChildBrowsingContext` methods. + nsTArray<RefPtr<BrowsingContext>> mChildren; + + // --- NEVER CHANGE `mNonSyntheticChildren` DIRECTLY! --- + // Same reason as for mChildren. + // mNonSyntheticChildren contains the same browsing contexts except browsing + // contexts created by the synthetic document for object loading contents + // loading images. This is used to discern browsing contexts created when + // loading images in <object> or <embed> elements, so that they can be hidden + // from named targeting, `Window.frames` etc. + nsTArray<RefPtr<BrowsingContext>> mNonSyntheticChildren; + + bool mIsDiscarded = false; + bool mIsInProcess = false; + + // Determines if we can execute scripts in this WindowContext. True if + // AllowJavascript() is true and script execution is allowed in the + // BrowsingContext. + bool mCanExecuteScripts = true; + + // The start time of user gesture, this is only available if the window + // context is in process. + TimeStamp mUserGestureStart; +}; + +using WindowContextTransaction = WindowContext::BaseTransaction; +using WindowContextInitializer = WindowContext::IPCInitializer; +using MaybeDiscardedWindowContext = MaybeDiscarded<WindowContext>; + +// Don't specialize the `Transaction` object for every translation unit it's +// used in. This should help keep code size down. +extern template class syncedcontext::Transaction<WindowContext>; + +} // namespace dom + +namespace ipc { +template <> +struct IPDLParamTraits<dom::MaybeDiscarded<dom::WindowContext>> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::MaybeDiscarded<dom::WindowContext>& aParam); + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + dom::MaybeDiscarded<dom::WindowContext>* aResult); +}; + +template <> +struct IPDLParamTraits<dom::WindowContext::IPCInitializer> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::WindowContext::IPCInitializer& aInitializer); + + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + dom::WindowContext::IPCInitializer* aInitializer); +}; +} // namespace ipc +} // namespace mozilla + +#endif // !defined(mozilla_dom_WindowContext_h) diff --git a/docshell/base/crashtests/1257730-1.html b/docshell/base/crashtests/1257730-1.html new file mode 100644 index 0000000000..028a1adb88 --- /dev/null +++ b/docshell/base/crashtests/1257730-1.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<!-- +user_pref("browser.send_pings", true); +--> +<script> + +function boom() { + var aLink = document.createElement('a'); + document.body.appendChild(aLink); + aLink.ping = "ping"; + aLink.href = "href"; + aLink.click(); + + var baseElement = document.createElement('base'); + baseElement.setAttribute("href", "javascript:void 0"); + document.head.appendChild(baseElement); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/docshell/base/crashtests/1331295.html b/docshell/base/crashtests/1331295.html new file mode 100644 index 0000000000..cdcb29e7fe --- /dev/null +++ b/docshell/base/crashtests/1331295.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> +function boom() { + setTimeout(function(){ + var o=document.getElementById('b'); + document.getElementById('a').appendChild(o.parentNode.removeChild(o)); + },0); + var o=document.getElementById('c'); + var p=document.getElementById('b'); + p.id=[o.id, o.id=p.id][0]; + o=document.getElementById('b'); + o.setAttribute('sandbox', 'disc'); + window.location.reload(true); +} +</script> +</head> +<body onload="boom();"> +<header id='a'></header> +<output id='b'></output> +<iframe id='c' sandbox='allow-same-origin' src='http://a'></iframe> +</body> +</html> diff --git a/docshell/base/crashtests/1341657.html b/docshell/base/crashtests/1341657.html new file mode 100644 index 0000000000..d68fa1eb03 --- /dev/null +++ b/docshell/base/crashtests/1341657.html @@ -0,0 +1,18 @@ +<html class="reftest-wait"> + <head> + <script> + function boom() { + o1 = document.createElement("script"); + o2 = document.implementation.createDocument('', '', null); + o3 = document.createElement("iframe"); + document.documentElement.appendChild(o3); + o4 = o3.contentWindow; + o5 = document.createTextNode('o2.adoptNode(o3); try { o4.location = "" } catch(e) {}'); + o1.appendChild(o5); + document.documentElement.appendChild(o1); + document.documentElement.classList.remove("reftest-wait"); + } + </script> + </head> + <body onload="boom();"></body> +</html> diff --git a/docshell/base/crashtests/1584467.html b/docshell/base/crashtests/1584467.html new file mode 100644 index 0000000000..5509808bcc --- /dev/null +++ b/docshell/base/crashtests/1584467.html @@ -0,0 +1,12 @@ +<script> +window.onload = () => { + a.addEventListener("DOMSubtreeModified", () => { + document.body.appendChild(b) + document.body.removeChild(b) + window[1] + }) + a.type = "" +} +</script> +<embed id="a"> +<iframe id="b"></iframe> diff --git a/docshell/base/crashtests/1614211-1.html b/docshell/base/crashtests/1614211-1.html new file mode 100644 index 0000000000..1d683e0714 --- /dev/null +++ b/docshell/base/crashtests/1614211-1.html @@ -0,0 +1,15 @@ +<script> +window.onload = () => { + b.addEventListener('DOMSubtreeModified', () => { + var o = document.getElementById('a') + var a = o.attributes + for (let j = 0; j < a.length; j++) { + o.setAttribute(a[j].name, 'i') + o.parentNode.appendChild(o) + } + }) + b.setAttribute('a', b) +} +</script> +<iframe id='a' sandbox='' allowfullscreen=''></iframe> +<dfn id='b'> diff --git a/docshell/base/crashtests/1617315-1.html b/docshell/base/crashtests/1617315-1.html new file mode 100644 index 0000000000..05d9a704dc --- /dev/null +++ b/docshell/base/crashtests/1617315-1.html @@ -0,0 +1,8 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + let o = document.getElementById('a') + o.setAttribute('id', '') + o.setAttribute('sandbox', '') +}) +</script> +<iframe id='a' sandbox='s' src='http://%CF'></iframe> diff --git a/docshell/base/crashtests/1667491.html b/docshell/base/crashtests/1667491.html new file mode 100644 index 0000000000..ecc77a5e9b --- /dev/null +++ b/docshell/base/crashtests/1667491.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> + <meta charset="UTF-8"> +<script> + function go() { + let win = window.open("1667491_1.html"); + win.finish = function() { + document.documentElement.removeAttribute("class"); + }; + } +</script> +</head> +<body onload="go()"> +</body> +</html> diff --git a/docshell/base/crashtests/1667491_1.html b/docshell/base/crashtests/1667491_1.html new file mode 100644 index 0000000000..3df3353f72 --- /dev/null +++ b/docshell/base/crashtests/1667491_1.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <script> + function go() { + document.body.appendChild(a) + window.frames[0].onbeforeunload = document.createElement("body").onload; + window.requestIdleCallback(() => { + window.close(); + finish(); + }); + } + </script> +</head> +<body onload="go()"> +<iframe id="a"></iframe> +<iframe></iframe> +</body> +</html> + diff --git a/docshell/base/crashtests/1672873.html b/docshell/base/crashtests/1672873.html new file mode 100644 index 0000000000..33aa92f0ef --- /dev/null +++ b/docshell/base/crashtests/1672873.html @@ -0,0 +1,6 @@ +<script> +document.addEventListener('DOMContentLoaded', () => { + var x = new Blob([undefined, ''], { }) + self.history.pushState(x, 'x', 'missing.file') +}) +</script> diff --git a/docshell/base/crashtests/1690169-1.html b/docshell/base/crashtests/1690169-1.html new file mode 100644 index 0000000000..6c9be20be3 --- /dev/null +++ b/docshell/base/crashtests/1690169-1.html @@ -0,0 +1,11 @@ +<script> +var woff = "data:font/woff2;base64,"; +for (let i = 0; i < 20000; i++) { + woff += "d09GMgABAAAA"; +} +window.onload = () => { + try { window.open('data:text/html,<spacer>', 'pu9', 'width=911').close() } catch (e) {} + document.getElementById('a').setAttribute('src', woff) +} +</script> +<iframe id='a' hidden src='http://a'></iframe> diff --git a/docshell/base/crashtests/1753136.html b/docshell/base/crashtests/1753136.html new file mode 100644 index 0000000000..22f679309e --- /dev/null +++ b/docshell/base/crashtests/1753136.html @@ -0,0 +1,2 @@ +<meta charset="UTF-8"> +<iframe sandbox='' src='http://🙅🎂งิ'></iframe> diff --git a/docshell/base/crashtests/1804803.html b/docshell/base/crashtests/1804803.html new file mode 100644 index 0000000000..5103c00416 --- /dev/null +++ b/docshell/base/crashtests/1804803.html @@ -0,0 +1,13 @@ +<html class="reftest-wait"> +<script> +function test() { + // If the reload in the iframe succeeds we might crash, so wait for it. + setTimeout(function() { + document.documentElement.className = ""; + }, 500); +} +</script> +<body onload="test()"> + <iframe src="1804803.sjs"></iframe> +</body> +</html> diff --git a/docshell/base/crashtests/1804803.sjs b/docshell/base/crashtests/1804803.sjs new file mode 100644 index 0000000000..0486e32048 --- /dev/null +++ b/docshell/base/crashtests/1804803.sjs @@ -0,0 +1,18 @@ +function handleRequest(request, response) { + let counter = Number(getState("load")); + const reload = counter == 0 ? "self.history.go(0);" : ""; + setState("load", String(++counter)); + const document = ` +<script> + document.addEventListener('DOMContentLoaded', () => { + ${reload} + let url = window.location.href; + for (let i = 0; i < 50; i++) { + self.history.pushState({x: i}, '', url + "#" + i); + } + }); +</script> +`; + + response.write(document); +} diff --git a/docshell/base/crashtests/369126-1.html b/docshell/base/crashtests/369126-1.html new file mode 100644 index 0000000000..e9dacec301 --- /dev/null +++ b/docshell/base/crashtests/369126-1.html @@ -0,0 +1,16 @@ +<html class="reftest-wait">
+<head>
+<script>
+function boom()
+{
+ document.getElementById("frameset").removeChild(document.getElementById("frame"));
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+
+<frameset id="frameset" onload="setTimeout(boom, 100)">
+ <frame id="frame" src="data:text/html,<body onUnload="location = 'http://www.mozilla.org/'">This frame's onunload tries to load another page.">
+</frameset>
+
+</html>
diff --git a/docshell/base/crashtests/40929-1-inner.html b/docshell/base/crashtests/40929-1-inner.html new file mode 100644 index 0000000000..313046a348 --- /dev/null +++ b/docshell/base/crashtests/40929-1-inner.html @@ -0,0 +1,14 @@ +<html><head><title>Infinite Loop</title></head> +<body onLoad="initNav(); initNav();"> + +<script language="JavaScript"> + +function initNav() { + ++parent.i; + if (parent.i < 10) + window.location.href=window.location.href; +} + +</script> + +</body></html> diff --git a/docshell/base/crashtests/40929-1.html b/docshell/base/crashtests/40929-1.html new file mode 100644 index 0000000000..90685d9f1f --- /dev/null +++ b/docshell/base/crashtests/40929-1.html @@ -0,0 +1,6 @@ +<html> +<head><title>Infinite Loop</title><script>var i=0;</script></head> +<body> +<iframe src="40929-1-inner.html"></iframe> +</body> +</html> diff --git a/docshell/base/crashtests/430124-1.html b/docshell/base/crashtests/430124-1.html new file mode 100644 index 0000000000..8cdbc1d077 --- /dev/null +++ b/docshell/base/crashtests/430124-1.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html> +<head></head> +<body onpagehide="document.getElementById('a').focus();"><div id="a"></div></body> +</html> diff --git a/docshell/base/crashtests/430628-1.html b/docshell/base/crashtests/430628-1.html new file mode 100644 index 0000000000..4a68a5a015 --- /dev/null +++ b/docshell/base/crashtests/430628-1.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<head> +</head> +<body onpagehide="document.body.removeChild(document.getElementById('s'));"> +<span id="s" contenteditable="true"></span> +</body> +</html> diff --git a/docshell/base/crashtests/432114-1.html b/docshell/base/crashtests/432114-1.html new file mode 100644 index 0000000000..8878d6605a --- /dev/null +++ b/docshell/base/crashtests/432114-1.html @@ -0,0 +1,8 @@ +<html>
+<head>
+<title>Bug - Crash [@ PL_DHashTableOperate] with DOMNodeInserted event listener removing window and frameset contenteditable</title>
+</head>
+<body>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Cscript%3E%0Awindow.addEventListener%28%27DOMNodeInserted%27%2C%20function%28%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%3B%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3Cframeset%20contenteditable%3D%22true%22%3E"></iframe>
+</body>
+</html>
diff --git a/docshell/base/crashtests/432114-2.html b/docshell/base/crashtests/432114-2.html new file mode 100644 index 0000000000..da77287b61 --- /dev/null +++ b/docshell/base/crashtests/432114-2.html @@ -0,0 +1,21 @@ +<html class="reftest-wait">
+<head>
+<title>testcase2 Bug 432114 � Crash [@ PL_DHashTableOperate] with DOMNodeInserted event listener removing window and frameset contenteditable</title>
+</head>
+<body>
+<script>
+ window.addEventListener("DOMNodeRemoved", function() {
+ setTimeout(function() {
+ document.documentElement.removeAttribute("class");
+ }, 0);
+ });
+ var iframe = document.getElementById("content");
+ iframe.onload=function() {
+ dump("iframe onload\n");
+ console.log("iframe onload");
+ };
+</script>
+<iframe id="content" src="file_432114-2.xhtml" style="width:1000px;height: 200px;"></iframe>
+
+</body>
+</html>
diff --git a/docshell/base/crashtests/436900-1-inner.html b/docshell/base/crashtests/436900-1-inner.html new file mode 100644 index 0000000000..6fe35ccb1a --- /dev/null +++ b/docshell/base/crashtests/436900-1-inner.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + +<meta http-equiv="refresh" content="0"> + +<script language="javascript"> + +location.hash += "+++"; + +function done() +{ + parent.document.documentElement.removeAttribute("class"); +} + +</script> +</head> +<body onload="setTimeout(done, 10)"> + +</body> +</html> diff --git a/docshell/base/crashtests/436900-1.html b/docshell/base/crashtests/436900-1.html new file mode 100644 index 0000000000..582d1919d1 --- /dev/null +++ b/docshell/base/crashtests/436900-1.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +</head> +<body> +<iframe src="436900-1-inner.html#foo"></iframe> +</body> +</html> diff --git a/docshell/base/crashtests/436900-2-inner.html b/docshell/base/crashtests/436900-2-inner.html new file mode 100644 index 0000000000..ea79f75e88 --- /dev/null +++ b/docshell/base/crashtests/436900-2-inner.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + +<meta http-equiv="refresh" content="0"> + +<script language="javascript" id="foo+++"> + +location.hash += "+++"; + +function done() +{ + parent.document.documentElement.removeAttribute("class"); +} + +</script> +</head> +<body onload="setTimeout(done, 10)"> + +</body> +</html> diff --git a/docshell/base/crashtests/436900-2.html b/docshell/base/crashtests/436900-2.html new file mode 100644 index 0000000000..2e1f0c1def --- /dev/null +++ b/docshell/base/crashtests/436900-2.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +</head> +<body> +<iframe src="436900-2-inner.html#foo"></iframe> +</body> +</html> diff --git a/docshell/base/crashtests/443655.html b/docshell/base/crashtests/443655.html new file mode 100644 index 0000000000..ce0a8c18b8 --- /dev/null +++ b/docshell/base/crashtests/443655.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +</head> + +<body onload="document.removeChild(document.documentElement)"> + +<!-- The order of the two iframes matters! --> + +<iframe src='data:text/html,<body onload="s = parent.document.getElementById('s').contentWindow;" onunload="s.location = s.location;">'></iframe> + +<iframe id="s"></iframe> + +</body> +</html> diff --git a/docshell/base/crashtests/500328-1.html b/docshell/base/crashtests/500328-1.html new file mode 100644 index 0000000000..fd97f84ae1 --- /dev/null +++ b/docshell/base/crashtests/500328-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<body onload="test();"> +<script> + function test() { + // Test that calling pushState() with a state object which calls + // history.back() doesn't crash. We need to make sure that there's at least + // one entry in the history before we do anything else. + history.pushState(null, ""); + + x = {}; + x.toJSON = { history.back(); return "{a:1}"; }; + history.pushState(x, ""); + } +</script> +</body> +</html> diff --git a/docshell/base/crashtests/514779-1.xhtml b/docshell/base/crashtests/514779-1.xhtml new file mode 100644 index 0000000000..16ac3d9d66 --- /dev/null +++ b/docshell/base/crashtests/514779-1.xhtml @@ -0,0 +1,9 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head></head> + +<body onunload="document.getElementById('tbody').appendChild(document.createElementNS('http://www.w3.org/1999/xhtml', 'span'))"> + <iframe/> + <tbody contenteditable="true" id="tbody">xy</tbody> +</body> + +</html> diff --git a/docshell/base/crashtests/614499-1.html b/docshell/base/crashtests/614499-1.html new file mode 100644 index 0000000000..7053a3f52f --- /dev/null +++ b/docshell/base/crashtests/614499-1.html @@ -0,0 +1,20 @@ +<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var f = document.getElementById("f");
+
+ for (var i = 0; i < 50; ++i) {
+ f.contentWindow.history.pushState({}, "");
+ }
+
+ document.body.removeChild(f);
+}
+
+</script>
+</head>
+<body onload="boom();"><iframe id="f" src="data:text/html,1"></iframe></body>
+</html>
\ No newline at end of file diff --git a/docshell/base/crashtests/678872-1.html b/docshell/base/crashtests/678872-1.html new file mode 100644 index 0000000000..294b3e689b --- /dev/null +++ b/docshell/base/crashtests/678872-1.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +var f1, f2; + +function b1() +{ + f1 = document.getElementById("f1"); + f2 = document.getElementById("f2"); + f1.contentWindow.document.write("11"); + f1.contentWindow.history.back(); + setTimeout(b2, 0); +} + +function b2() +{ + f2.contentWindow.history.forward(); + f2.contentWindow.location.reload(); + f1.remove(); +} + +</script> + + +</script> +</head> + +<body onload="setTimeout(b1, 0);"> + +<iframe id="f1" src="data:text/html,1"></iframe> +<iframe id="f2" src="data:text/html,2"></iframe> + +</body> +</html> diff --git a/docshell/base/crashtests/914521.html b/docshell/base/crashtests/914521.html new file mode 100644 index 0000000000..f30d78c10f --- /dev/null +++ b/docshell/base/crashtests/914521.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<meta charset="UTF-8"> +<script> + +function f() +{ + function spin() { + for (var i = 0; i < 8; ++i) { + var x = new XMLHttpRequest(); + x.open('GET', 'data:text/html,' + i, false); + x.send(); + } + } + + window.addEventListener("popstate", spin); + window.close(); + window.location = "#c"; + document.documentElement.removeAttribute("class"); +} + +function start() +{ + var win = window.open("javascript:'<html><body>dummy</body></html>';", null, "width=300,height=300"); + win.onload = f; +} + +</script> +</head> +<body onload="start();"></body> +</html> diff --git a/docshell/base/crashtests/crashtests.list b/docshell/base/crashtests/crashtests.list new file mode 100644 index 0000000000..f9b214bfa2 --- /dev/null +++ b/docshell/base/crashtests/crashtests.list @@ -0,0 +1,25 @@ +load 40929-1.html +load 369126-1.html +load 430124-1.html +load 430628-1.html +load 432114-1.html +load 432114-2.html +load 436900-1.html +asserts(0-1) load 436900-2.html # bug 566159 +load 443655.html +load 500328-1.html +load 514779-1.xhtml +load 614499-1.html +load 678872-1.html +skip-if(Android) pref(dom.disable_open_during_load,false) load 914521.html # Android bug 1584562 +pref(browser.send_pings,true) asserts(0-2) load 1257730-1.html # bug 566159 +load 1331295.html +load 1341657.html +load 1584467.html +load 1614211-1.html +load 1617315-1.html +skip-if(Android) pref(dom.disable_open_during_load,false) load 1667491.html +pref(dom.disable_open_during_load,false) load 1690169-1.html +load 1672873.html +load 1753136.html +HTTP load 1804803.html diff --git a/docshell/base/crashtests/file_432114-2.xhtml b/docshell/base/crashtests/file_432114-2.xhtml new file mode 100644 index 0000000000..40bf886b8e --- /dev/null +++ b/docshell/base/crashtests/file_432114-2.xhtml @@ -0,0 +1 @@ +<html xmlns='http://www.w3.org/1999/xhtml'><frameset contenteditable='true'/><script>function doExecCommand(){dump("doExecCommand\n");document.execCommand('formatBlock', false, 'p');}setTimeout(doExecCommand,100); window.addEventListener('DOMNodeRemoved', function() {window.frameElement.parentNode.removeChild(window.frameElement);}, true);</script></html> diff --git a/docshell/base/moz.build b/docshell/base/moz.build new file mode 100644 index 0000000000..049a78d440 --- /dev/null +++ b/docshell/base/moz.build @@ -0,0 +1,130 @@ +# -*- 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", "DOM: Navigation") + +with Files("crashtests/430628*"): + BUG_COMPONENT = ("Core", "DOM: Editor") + +with Files("crashtests/432114*"): + BUG_COMPONENT = ("Core", "DOM: Editor") + +with Files("crashtests/500328*"): + BUG_COMPONENT = ("Firefox", "Bookmarks & History") + +with Files("IHistory.h"): + BUG_COMPONENT = ("Toolkit", "Places") + +with Files("*LoadContext.*"): + BUG_COMPONENT = ("Core", "Networking") + +with Files("nsAboutRedirector.*"): + BUG_COMPONENT = ("Core", "General") + +with Files("nsIScrollObserver.*"): + BUG_COMPONENT = ("Core", "Panning and Zooming") + +DIRS += [ + "timeline", +] + +XPIDL_SOURCES += [ + "nsIContentViewer.idl", + "nsIContentViewerEdit.idl", + "nsIDocShell.idl", + "nsIDocShellTreeItem.idl", + "nsIDocShellTreeOwner.idl", + "nsIDocumentLoaderFactory.idl", + "nsILoadContext.idl", + "nsILoadURIDelegate.idl", + "nsIPrivacyTransitionObserver.idl", + "nsIReflowObserver.idl", + "nsIRefreshURI.idl", + "nsITooltipListener.idl", + "nsITooltipTextProvider.idl", + "nsIURIFixup.idl", + "nsIWebNavigation.idl", + "nsIWebNavigationInfo.idl", + "nsIWebPageDescriptor.idl", +] + +XPIDL_MODULE = "docshell" + +EXPORTS += [ + "nsCTooltipTextProvider.h", + "nsDocShell.h", + "nsDocShellLoadState.h", + "nsDocShellLoadTypes.h", + "nsDocShellTreeOwner.h", + "nsDSURIContentListener.h", + "nsIScrollObserver.h", + "nsWebNavigationInfo.h", + "SerializedLoadContext.h", +] + +EXPORTS.mozilla += [ + "BaseHistory.h", + "IHistory.h", + "LoadContext.h", +] + +EXPORTS.mozilla.dom += [ + "BrowsingContext.h", + "BrowsingContextGroup.h", + "BrowsingContextWebProgress.h", + "CanonicalBrowsingContext.h", + "ChildProcessChannelListener.h", + "SyncedContext.h", + "SyncedContextInlines.h", + "WindowContext.h", +] + +UNIFIED_SOURCES += [ + "BaseHistory.cpp", + "BrowsingContext.cpp", + "BrowsingContextGroup.cpp", + "BrowsingContextWebProgress.cpp", + "CanonicalBrowsingContext.cpp", + "ChildProcessChannelListener.cpp", + "LoadContext.cpp", + "nsAboutRedirector.cpp", + "nsDocShell.cpp", + "nsDocShellEditorData.cpp", + "nsDocShellEnumerator.cpp", + "nsDocShellLoadState.cpp", + "nsDocShellTelemetryUtils.cpp", + "nsDocShellTreeOwner.cpp", + "nsDSURIContentListener.cpp", + "nsPingListener.cpp", + "nsRefreshTimer.cpp", + "nsWebNavigationInfo.cpp", + "SerializedLoadContext.cpp", + "WindowContext.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/docshell/shistory", + "/dom/base", + "/dom/bindings", + "/js/xpconnect/src", + "/layout/base", + "/layout/generic", + "/layout/style", + "/layout/xul", + "/netwerk/base", + "/netwerk/protocol/viewsource", + "/toolkit/components/browser", + "/toolkit/components/find", + "/tools/profiler", +] + +EXTRA_JS_MODULES += ["URIFixup.sys.mjs"] + +include("/tools/fuzzing/libfuzzer-config.mozbuild") diff --git a/docshell/base/nsAboutRedirector.cpp b/docshell/base/nsAboutRedirector.cpp new file mode 100644 index 0000000000..faf334f345 --- /dev/null +++ b/docshell/base/nsAboutRedirector.cpp @@ -0,0 +1,299 @@ +/* -*- 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/. */ + +#include "nsAboutRedirector.h" +#include "nsNetUtil.h" +#include "nsAboutProtocolUtils.h" +#include "nsBaseChannel.h" +#include "mozilla/ArrayUtils.h" +#include "nsIProtocolHandler.h" +#include "nsXULAppAPI.h" +#include "mozilla/Preferences.h" +#include "mozilla/gfx/GPUProcessManager.h" + +#define ABOUT_CONFIG_ENABLED_PREF "general.aboutConfig.enable" + +NS_IMPL_ISUPPORTS(nsAboutRedirector, nsIAboutModule) + +struct RedirEntry { + const char* id; + const char* url; + uint32_t flags; +}; + +class CrashChannel final : public nsBaseChannel { + public: + explicit CrashChannel(nsIURI* aURI) { SetURI(aURI); } + + nsresult OpenContentStream(bool async, nsIInputStream** stream, + nsIChannel** channel) override { + nsAutoCString spec; + mURI->GetSpec(spec); + + if (spec.EqualsASCII("about:crashparent") && XRE_IsParentProcess()) { + MOZ_CRASH("Crash via about:crashparent"); + } + + if (spec.EqualsASCII("about:crashgpu") && XRE_IsParentProcess()) { + if (auto* gpu = mozilla::gfx::GPUProcessManager::Get()) { + gpu->CrashProcess(); + } + } + + if (spec.EqualsASCII("about:crashcontent") && XRE_IsContentProcess()) { + MOZ_CRASH("Crash via about:crashcontent"); + } + + NS_WARNING("Unhandled about:crash* URI or wrong process"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + protected: + virtual ~CrashChannel() = default; +}; + +/* + Entries which do not have URI_SAFE_FOR_UNTRUSTED_CONTENT will run with chrome + privileges. This is potentially dangerous. Please use + URI_SAFE_FOR_UNTRUSTED_CONTENT in the third argument to each map item below + unless your about: page really needs chrome privileges. Security review is + required before adding new map entries without + URI_SAFE_FOR_UNTRUSTED_CONTENT. + + URI_SAFE_FOR_UNTRUSTED_CONTENT is not enough to let web pages load that page, + for that you need MAKE_LINKABLE. + + NOTE: changes to this redir map need to be accompanied with changes to + docshell/build/components.conf + */ +static const RedirEntry kRedirMap[] = { + {"about", "chrome://global/content/aboutAbout.html", 0}, + {"addons", "chrome://mozapps/content/extensions/aboutaddons.html", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, + {"buildconfig", "chrome://global/content/buildconfig.html", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::IS_SECURE_CHROME_UI}, + {"checkerboard", "chrome://global/content/aboutCheckerboard.html", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::ALLOW_SCRIPT}, +#ifndef MOZ_WIDGET_ANDROID + {"config", "chrome://global/content/aboutconfig/aboutconfig.html", + nsIAboutModule::IS_SECURE_CHROME_UI}, +#else + {"config", "chrome://geckoview/content/config.xhtml", + nsIAboutModule::IS_SECURE_CHROME_UI}, +#endif +#ifdef MOZ_CRASHREPORTER + {"crashes", "chrome://global/content/crashes.html", + nsIAboutModule::IS_SECURE_CHROME_UI}, +#endif + {"credits", "https://www.mozilla.org/credits/", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_MUST_LOAD_IN_CHILD}, + {"httpsonlyerror", "chrome://global/content/httpsonlyerror/errorpage.html", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT | + nsIAboutModule::HIDE_FROM_ABOUTABOUT}, + {"license", "chrome://global/content/license.html", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::IS_SECURE_CHROME_UI}, + {"logging", "chrome://global/content/aboutLogging.html", + nsIAboutModule::ALLOW_SCRIPT}, + {"logo", "chrome://branding/content/about.png", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + // Linkable for testing reasons. + nsIAboutModule::MAKE_LINKABLE}, + {"memory", "chrome://global/content/aboutMemory.xhtml", + nsIAboutModule::ALLOW_SCRIPT}, + {"certificate", "chrome://global/content/certviewer/certviewer.html", + nsIAboutModule::ALLOW_SCRIPT | + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_MUST_LOAD_IN_CHILD | + nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS | + nsIAboutModule::IS_SECURE_CHROME_UI}, + {"mozilla", "chrome://global/content/mozilla.html", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT}, + {"neterror", "chrome://global/content/aboutNetError.xhtml", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT | + nsIAboutModule::HIDE_FROM_ABOUTABOUT}, + {"networking", "chrome://global/content/aboutNetworking.html", + nsIAboutModule::ALLOW_SCRIPT}, + {"performance", "chrome://global/content/aboutPerformance.html", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, +#ifndef ANDROID + {"plugins", "chrome://global/content/plugins.html", + nsIAboutModule::URI_MUST_LOAD_IN_CHILD | + nsIAboutModule::IS_SECURE_CHROME_UI}, +#endif + {"processes", "chrome://global/content/aboutProcesses.html", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, + // about:serviceworkers always wants to load in the parent process because + // the only place nsIServiceWorkerManager has any data is in the parent + // process. + // + // There is overlap without about:debugging, but about:debugging is not + // available on mobile at this time, and it's useful to be able to know if + // a ServiceWorker is registered directly from the mobile browser without + // having to connect the device to a desktop machine and all that entails. + {"serviceworkers", "chrome://global/content/aboutServiceWorkers.xhtml", + nsIAboutModule::ALLOW_SCRIPT}, +#ifndef ANDROID + {"profiles", "chrome://global/content/aboutProfiles.xhtml", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, +#endif + // about:srcdoc is unresolvable by specification. It is included here + // because the security manager would disallow srcdoc iframes otherwise. + {"srcdoc", "about:blank", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::HIDE_FROM_ABOUTABOUT | + // Needs to be linkable so content can touch its own srcdoc frames + nsIAboutModule::MAKE_LINKABLE | nsIAboutModule::URI_CAN_LOAD_IN_CHILD}, + {"support", "chrome://global/content/aboutSupport.xhtml", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, +#ifdef XP_WIN + {"third-party", "chrome://global/content/aboutThirdParty.html", + nsIAboutModule::ALLOW_SCRIPT}, + {"windows-messages", "chrome://global/content/aboutWindowsMessages.html", + nsIAboutModule::ALLOW_SCRIPT}, +#endif +#ifndef MOZ_GLEAN_ANDROID + {"glean", "chrome://global/content/aboutGlean.html", +# if !defined(NIGHTLY_BUILD) && defined(MOZILLA_OFFICIAL) + nsIAboutModule::HIDE_FROM_ABOUTABOUT | +# endif + nsIAboutModule::ALLOW_SCRIPT}, +#endif + {"telemetry", "chrome://global/content/aboutTelemetry.xhtml", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, + {"translations", "chrome://global/content/translations/translations.html", + nsIAboutModule::ALLOW_SCRIPT | + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_MUST_LOAD_IN_CHILD | + nsIAboutModule::HIDE_FROM_ABOUTABOUT}, + {"url-classifier", "chrome://global/content/aboutUrlClassifier.xhtml", + nsIAboutModule::ALLOW_SCRIPT}, + {"webrtc", "chrome://global/content/aboutwebrtc/aboutWebrtc.html", + nsIAboutModule::ALLOW_SCRIPT}, + {"crashparent", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT}, + {"crashcontent", "about:blank", + nsIAboutModule::HIDE_FROM_ABOUTABOUT | + nsIAboutModule::URI_CAN_LOAD_IN_CHILD | + nsIAboutModule::URI_MUST_LOAD_IN_CHILD}, + {"crashgpu", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT}}; +static const int kRedirTotal = mozilla::ArrayLength(kRedirMap); + +NS_IMETHODIMP +nsAboutRedirector::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** aResult) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aLoadInfo); + NS_ASSERTION(aResult, "must not be null"); + + nsAutoCString path; + nsresult rv = NS_GetAboutModuleName(aURI, path); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (path.EqualsASCII("crashparent") || path.EqualsASCII("crashcontent") || + path.EqualsASCII("crashgpu")) { + bool isExternal; + aLoadInfo->GetLoadTriggeredFromExternal(&isExternal); + if (isExternal) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIChannel> channel = new CrashChannel(aURI); + channel->SetLoadInfo(aLoadInfo); + channel.forget(aResult); + return NS_OK; + } + + if (path.EqualsASCII("config") && + !mozilla::Preferences::GetBool(ABOUT_CONFIG_ENABLED_PREF, true)) { + return NS_ERROR_NOT_AVAILABLE; + } + + for (int i = 0; i < kRedirTotal; i++) { + if (!strcmp(path.get(), kRedirMap[i].id)) { + nsCOMPtr<nsIChannel> tempChannel; + nsCOMPtr<nsIURI> tempURI; + rv = NS_NewURI(getter_AddRefs(tempURI), kRedirMap[i].url); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewChannelInternal(getter_AddRefs(tempChannel), tempURI, + aLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + // If tempURI links to an external URI (i.e. something other than + // chrome:// or resource://) then set result principal URI on the + // load info which forces the channel principal to reflect the displayed + // URL rather then being the systemPrincipal. + bool isUIResource = false; + rv = NS_URIChainHasFlags(tempURI, nsIProtocolHandler::URI_IS_UI_RESOURCE, + &isUIResource); + NS_ENSURE_SUCCESS(rv, rv); + + bool isAboutBlank = NS_IsAboutBlank(tempURI); + + if (!isUIResource && !isAboutBlank) { + aLoadInfo->SetResultPrincipalURI(tempURI); + } + + tempChannel->SetOriginalURI(aURI); + + tempChannel.forget(aResult); + return rv; + } + } + + NS_ERROR("nsAboutRedirector called for unknown case"); + return NS_ERROR_ILLEGAL_VALUE; +} + +NS_IMETHODIMP +nsAboutRedirector::GetURIFlags(nsIURI* aURI, uint32_t* aResult) { + NS_ENSURE_ARG_POINTER(aURI); + + nsAutoCString name; + nsresult rv = NS_GetAboutModuleName(aURI, name); + NS_ENSURE_SUCCESS(rv, rv); + + for (int i = 0; i < kRedirTotal; i++) { + if (name.EqualsASCII(kRedirMap[i].id)) { + *aResult = kRedirMap[i].flags; + return NS_OK; + } + } + + NS_ERROR("nsAboutRedirector called for unknown case"); + return NS_ERROR_ILLEGAL_VALUE; +} + +NS_IMETHODIMP +nsAboutRedirector::GetChromeURI(nsIURI* aURI, nsIURI** chromeURI) { + NS_ENSURE_ARG_POINTER(aURI); + + nsAutoCString name; + nsresult rv = NS_GetAboutModuleName(aURI, name); + NS_ENSURE_SUCCESS(rv, rv); + + for (const auto& redir : kRedirMap) { + if (name.EqualsASCII(redir.id)) { + return NS_NewURI(chromeURI, redir.url); + } + } + + NS_ERROR("nsAboutRedirector called for unknown case"); + return NS_ERROR_ILLEGAL_VALUE; +} + +nsresult nsAboutRedirector::Create(REFNSIID aIID, void** aResult) { + RefPtr<nsAboutRedirector> about = new nsAboutRedirector(); + return about->QueryInterface(aIID, aResult); +} diff --git a/docshell/base/nsAboutRedirector.h b/docshell/base/nsAboutRedirector.h new file mode 100644 index 0000000000..0bf021cc31 --- /dev/null +++ b/docshell/base/nsAboutRedirector.h @@ -0,0 +1,26 @@ +/* -*- 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/. */ + +#ifndef nsAboutRedirector_h__ +#define nsAboutRedirector_h__ + +#include "nsIAboutModule.h" + +class nsAboutRedirector : public nsIAboutModule { + public: + NS_DECL_ISUPPORTS + + NS_DECL_NSIABOUTMODULE + + nsAboutRedirector() {} + + static nsresult Create(REFNSIID aIID, void** aResult); + + protected: + virtual ~nsAboutRedirector() {} +}; + +#endif // nsAboutRedirector_h__ diff --git a/docshell/base/nsCTooltipTextProvider.h b/docshell/base/nsCTooltipTextProvider.h new file mode 100644 index 0000000000..731edf1170 --- /dev/null +++ b/docshell/base/nsCTooltipTextProvider.h @@ -0,0 +1,15 @@ +/* -*- 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/. */ + +#ifndef NSCTOOLTIPTEXTPROVIDER_H +#define NSCTOOLTIPTEXTPROVIDER_H + +#define NS_TOOLTIPTEXTPROVIDER_CONTRACTID \ + "@mozilla.org/embedcomp/tooltiptextprovider;1" +#define NS_DEFAULTTOOLTIPTEXTPROVIDER_CONTRACTID \ + "@mozilla.org/embedcomp/default-tooltiptextprovider;1" + +#endif diff --git a/docshell/base/nsDSURIContentListener.cpp b/docshell/base/nsDSURIContentListener.cpp new file mode 100644 index 0000000000..437f729fc0 --- /dev/null +++ b/docshell/base/nsDSURIContentListener.cpp @@ -0,0 +1,297 @@ +/* -*- 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/. */ + +#include "nsDocShell.h" +#include "nsDSURIContentListener.h" +#include "nsIChannel.h" +#include "nsServiceManagerUtils.h" +#include "nsDocShellCID.h" +#include "nsIWebNavigationInfo.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/Unused.h" +#include "nsError.h" +#include "nsContentSecurityManager.h" +#include "nsDocShellLoadTypes.h" +#include "nsGlobalWindowOuter.h" +#include "nsIInterfaceRequestor.h" +#include "nsIMultiPartChannel.h" +#include "nsWebNavigationInfo.h" + +using namespace mozilla; +using namespace mozilla::dom; + +NS_IMPL_ADDREF(MaybeCloseWindowHelper) +NS_IMPL_RELEASE(MaybeCloseWindowHelper) + +NS_INTERFACE_MAP_BEGIN(MaybeCloseWindowHelper) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsINamed) +NS_INTERFACE_MAP_END + +MaybeCloseWindowHelper::MaybeCloseWindowHelper(BrowsingContext* aContentContext) + : mBrowsingContext(aContentContext), + mTimer(nullptr), + mShouldCloseWindow(false) {} + +MaybeCloseWindowHelper::~MaybeCloseWindowHelper() {} + +void MaybeCloseWindowHelper::SetShouldCloseWindow(bool aShouldCloseWindow) { + mShouldCloseWindow = aShouldCloseWindow; +} + +BrowsingContext* MaybeCloseWindowHelper::MaybeCloseWindow() { + if (!mShouldCloseWindow) { + return mBrowsingContext; + } + + // This method should not be called more than once, but it's better to avoid + // closing the current window again. + mShouldCloseWindow = false; + + // Reset the window context to the opener window so that the dependent + // dialogs have a parent + RefPtr<BrowsingContext> newBC = ChooseNewBrowsingContext(mBrowsingContext); + + if (newBC != mBrowsingContext && newBC && !newBC->IsDiscarded()) { + mBCToClose = mBrowsingContext; + mBrowsingContext = newBC; + + // Now close the old window. Do it on a timer so that we don't run + // into issues trying to close the window before it has fully opened. + NS_ASSERTION(!mTimer, "mTimer was already initialized once!"); + NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, 0, + nsITimer::TYPE_ONE_SHOT); + } + + return mBrowsingContext; +} + +already_AddRefed<BrowsingContext> +MaybeCloseWindowHelper::ChooseNewBrowsingContext(BrowsingContext* aBC) { + RefPtr<BrowsingContext> opener = aBC->GetOpener(); + if (opener && !opener->IsDiscarded()) { + return opener.forget(); + } + + if (!XRE_IsParentProcess()) { + return nullptr; + } + + opener = BrowsingContext::Get(aBC->Canonical()->GetCrossGroupOpenerId()); + if (!opener || opener->IsDiscarded()) { + return nullptr; + } + return opener.forget(); +} + +NS_IMETHODIMP +MaybeCloseWindowHelper::Notify(nsITimer* timer) { + NS_ASSERTION(mBCToClose, "No window to close after timer fired"); + + mBCToClose->Close(CallerType::System, IgnoreErrors()); + mBCToClose = nullptr; + mTimer = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +MaybeCloseWindowHelper::GetName(nsACString& aName) { + aName.AssignLiteral("MaybeCloseWindowHelper"); + return NS_OK; +} + +nsDSURIContentListener::nsDSURIContentListener(nsDocShell* aDocShell) + : mDocShell(aDocShell), + mExistingJPEGRequest(nullptr), + mParentContentListener(nullptr) {} + +nsDSURIContentListener::~nsDSURIContentListener() {} + +NS_IMPL_ADDREF(nsDSURIContentListener) +NS_IMPL_RELEASE(nsDSURIContentListener) + +NS_INTERFACE_MAP_BEGIN(nsDSURIContentListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURIContentListener) + NS_INTERFACE_MAP_ENTRY(nsIURIContentListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP +nsDSURIContentListener::DoContent(const nsACString& aContentType, + bool aIsContentPreferred, + nsIRequest* aRequest, + nsIStreamListener** aContentHandler, + bool* aAbortProcess) { + nsresult rv; + NS_ENSURE_ARG_POINTER(aContentHandler); + NS_ENSURE_TRUE(mDocShell, NS_ERROR_FAILURE); + RefPtr<nsDocShell> docShell = mDocShell; + + *aAbortProcess = false; + + // determine if the channel has just been retargeted to us... + nsLoadFlags loadFlags = 0; + if (nsCOMPtr<nsIChannel> openedChannel = do_QueryInterface(aRequest)) { + openedChannel->GetLoadFlags(&loadFlags); + } + + if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) { + // XXX: Why does this not stop the content too? + docShell->Stop(nsIWebNavigation::STOP_NETWORK); + NS_ENSURE_TRUE(mDocShell, NS_ERROR_FAILURE); + docShell->SetLoadType(aIsContentPreferred ? LOAD_LINK : LOAD_NORMAL); + } + + // In case of multipart jpeg request (mjpeg) we don't really want to + // create new viewer since the one we already have is capable of + // rendering multipart jpeg correctly (see bug 625012) + nsCOMPtr<nsIChannel> baseChannel; + if (nsCOMPtr<nsIMultiPartChannel> mpchan = do_QueryInterface(aRequest)) { + mpchan->GetBaseChannel(getter_AddRefs(baseChannel)); + } + + bool reuseCV = baseChannel && baseChannel == mExistingJPEGRequest && + aContentType.EqualsLiteral("image/jpeg"); + + if (mExistingJPEGStreamListener && reuseCV) { + RefPtr<nsIStreamListener> copy(mExistingJPEGStreamListener); + copy.forget(aContentHandler); + rv = NS_OK; + } else { + rv = docShell->CreateContentViewer(aContentType, aRequest, aContentHandler); + if (NS_SUCCEEDED(rv) && reuseCV) { + mExistingJPEGStreamListener = *aContentHandler; + } else { + mExistingJPEGStreamListener = nullptr; + } + mExistingJPEGRequest = baseChannel; + } + + if (rv == NS_ERROR_DOCSHELL_DYING) { + aRequest->Cancel(rv); + *aAbortProcess = true; + return NS_OK; + } + + if (NS_FAILED(rv)) { + // we don't know how to handle the content + nsCOMPtr<nsIStreamListener> forget = dont_AddRef(*aContentHandler); + *aContentHandler = nullptr; + return rv; + } + + if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) { + nsCOMPtr<nsPIDOMWindowOuter> domWindow = + mDocShell ? mDocShell->GetWindow() : nullptr; + NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE); + domWindow->Focus(mozilla::dom::CallerType::System); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDSURIContentListener::IsPreferred(const char* aContentType, + char** aDesiredContentType, + bool* aCanHandle) { + NS_ENSURE_ARG_POINTER(aCanHandle); + NS_ENSURE_ARG_POINTER(aDesiredContentType); + + // the docshell has no idea if it is the preferred content provider or not. + // It needs to ask its parent if it is the preferred content handler or not... + + nsCOMPtr<nsIURIContentListener> parentListener; + GetParentContentListener(getter_AddRefs(parentListener)); + if (parentListener) { + return parentListener->IsPreferred(aContentType, aDesiredContentType, + aCanHandle); + } + // we used to return false here if we didn't have a parent properly registered + // at the top of the docshell hierarchy to dictate what content types this + // docshell should be a preferred handler for. But this really makes it hard + // for developers using iframe or browser tags because then they need to make + // sure they implement nsIURIContentListener otherwise all link clicks would + // get sent to another window because we said we weren't the preferred handler + // type. I'm going to change the default now... if we can handle the content, + // and someone didn't EXPLICITLY set a nsIURIContentListener at the top of our + // docshell chain, then we'll now always attempt to process the content + // ourselves... + return CanHandleContent(aContentType, true, aDesiredContentType, aCanHandle); +} + +NS_IMETHODIMP +nsDSURIContentListener::CanHandleContent(const char* aContentType, + bool aIsContentPreferred, + char** aDesiredContentType, + bool* aCanHandleContent) { + MOZ_ASSERT(aCanHandleContent, "Null out param?"); + NS_ENSURE_ARG_POINTER(aDesiredContentType); + + *aCanHandleContent = false; + *aDesiredContentType = nullptr; + + if (aContentType) { + uint32_t canHandle = + nsWebNavigationInfo::IsTypeSupported(nsDependentCString(aContentType)); + *aCanHandleContent = (canHandle != nsIWebNavigationInfo::UNSUPPORTED); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDSURIContentListener::GetLoadCookie(nsISupports** aLoadCookie) { + NS_IF_ADDREF(*aLoadCookie = nsDocShell::GetAsSupports(mDocShell)); + return NS_OK; +} + +NS_IMETHODIMP +nsDSURIContentListener::SetLoadCookie(nsISupports* aLoadCookie) { +#ifdef DEBUG + RefPtr<nsDocLoader> cookieAsDocLoader = + nsDocLoader::GetAsDocLoader(aLoadCookie); + NS_ASSERTION(cookieAsDocLoader && cookieAsDocLoader == mDocShell, + "Invalid load cookie being set!"); +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsDSURIContentListener::GetParentContentListener( + nsIURIContentListener** aParentListener) { + if (mWeakParentContentListener) { + nsCOMPtr<nsIURIContentListener> tempListener = + do_QueryReferent(mWeakParentContentListener); + *aParentListener = tempListener; + NS_IF_ADDREF(*aParentListener); + } else { + *aParentListener = mParentContentListener; + NS_IF_ADDREF(*aParentListener); + } + return NS_OK; +} + +NS_IMETHODIMP +nsDSURIContentListener::SetParentContentListener( + nsIURIContentListener* aParentListener) { + if (aParentListener) { + // Store the parent listener as a weak ref. Parents not supporting + // nsISupportsWeakReference assert but may still be used. + mParentContentListener = nullptr; + mWeakParentContentListener = do_GetWeakReference(aParentListener); + if (!mWeakParentContentListener) { + mParentContentListener = aParentListener; + } + } else { + mWeakParentContentListener = nullptr; + mParentContentListener = nullptr; + } + return NS_OK; +} diff --git a/docshell/base/nsDSURIContentListener.h b/docshell/base/nsDSURIContentListener.h new file mode 100644 index 0000000000..61ed36456f --- /dev/null +++ b/docshell/base/nsDSURIContentListener.h @@ -0,0 +1,100 @@ +/* -*- 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/. */ + +#ifndef nsDSURIContentListener_h__ +#define nsDSURIContentListener_h__ + +#include "nsCOMPtr.h" +#include "nsIURIContentListener.h" +#include "nsWeakReference.h" +#include "nsITimer.h" + +class nsDocShell; +class nsIInterfaceRequestor; +class nsIWebNavigationInfo; +class nsPIDOMWindowOuter; + +// Helper Class to eventually close an already opened window +class MaybeCloseWindowHelper final : public nsITimerCallback, public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + explicit MaybeCloseWindowHelper( + mozilla::dom::BrowsingContext* aContentContext); + + /** + * Closes the provided window async (if mShouldCloseWindow is true) and + * returns a valid browsingContext to be used instead as parent for dialogs or + * similar things. + * In case mShouldCloseWindow is true, the returned BrowsingContext will be + * the window's opener (or original cross-group opener in the case of a + * `noopener` popup). + */ + mozilla::dom::BrowsingContext* MaybeCloseWindow(); + + void SetShouldCloseWindow(bool aShouldCloseWindow); + + protected: + ~MaybeCloseWindowHelper(); + + private: + already_AddRefed<mozilla::dom::BrowsingContext> ChooseNewBrowsingContext( + mozilla::dom::BrowsingContext* aBC); + + /** + * The dom window associated to handle content. + */ + RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext; + + /** + * Used to close the window on a timer, to avoid any exceptions that are + * thrown if we try to close the window before it's fully loaded. + */ + RefPtr<mozilla::dom::BrowsingContext> mBCToClose; + nsCOMPtr<nsITimer> mTimer; + + /** + * This is set based on whether the channel indicates that a new window + * was opened, e.g. for a download, or was blocked. If so, then we + * close it. + */ + bool mShouldCloseWindow; +}; + +class nsDSURIContentListener final : public nsIURIContentListener, + public nsSupportsWeakReference { + friend class nsDocShell; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIURICONTENTLISTENER + + protected: + explicit nsDSURIContentListener(nsDocShell* aDocShell); + virtual ~nsDSURIContentListener(); + + void DropDocShellReference() { + mDocShell = nullptr; + mExistingJPEGRequest = nullptr; + mExistingJPEGStreamListener = nullptr; + } + + protected: + nsDocShell* mDocShell; + // Hack to handle multipart images without creating a new viewer + nsCOMPtr<nsIStreamListener> mExistingJPEGStreamListener; + nsCOMPtr<nsIChannel> mExistingJPEGRequest; + + // Store the parent listener in either of these depending on + // if supports weak references or not. Proper weak refs are + // preferred and encouraged! + nsWeakPtr mWeakParentContentListener; + nsIURIContentListener* mParentContentListener; +}; + +#endif /* nsDSURIContentListener_h__ */ diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp new file mode 100644 index 0000000000..57ee34cad0 --- /dev/null +++ b/docshell/base/nsDocShell.cpp @@ -0,0 +1,13877 @@ +/* -*- 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/. */ + +#include "nsDocShell.h" + +#include <algorithm> + +#ifdef XP_WIN +# include <process.h> +# define getpid _getpid +#else +# include <unistd.h> // for getpid() +#endif + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Casting.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Components.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Encoding.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/InputTaskManager.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/Logging.h" +#include "mozilla/MediaFeatureChange.h" +#include "mozilla/ObservedDocShell.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/ScrollTypes.h" +#include "mozilla/SimpleEnumerator.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_docshell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/StartupTimeline.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/Telemetry.h" + +#include "mozilla/Unused.h" +#include "mozilla/WidgetUtils.h" + +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/ChildProcessChannelListener.h" +#include "mozilla/dom/ClientChannelHelper.h" +#include "mozilla/dom/ClientHandle.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/ClientManager.h" +#include "mozilla/dom/ClientSource.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentFrameMessageManager.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLAnchorElement.h" +#include "mozilla/dom/HTMLIFrameElement.h" +#include "mozilla/dom/PerformanceNavigation.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/dom/PopupBlocker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" +#include "mozilla/dom/ScreenOrientation.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ServiceWorkerInterceptController.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/SessionHistoryEntry.h" +#include "mozilla/dom/SessionStorageManager.h" +#include "mozilla/dom/SessionStoreChangeListener.h" +#include "mozilla/dom/SessionStoreChild.h" +#include "mozilla/dom/SessionStoreUtils.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/ChildSHistory.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/nsHTTPSOnlyUtils.h" +#include "mozilla/dom/LoadURIOptionsBinding.h" +#include "mozilla/dom/JSWindowActorChild.h" +#include "mozilla/dom/DocumentBinding.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/net/DocumentChannel.h" +#include "mozilla/net/DocumentChannelChild.h" +#include "mozilla/net/ParentChannelWrapper.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "ReferrerInfo.h" + +#include "nsIAuthPrompt.h" +#include "nsIAuthPrompt2.h" +#include "nsICachingChannel.h" +#include "nsICaptivePortalService.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" +#include "nsIClassOfService.h" +#include "nsIConsoleReportCollector.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIContentViewer.h" +#include "nsIController.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "mozilla/dom/Document.h" +#include "nsHTMLDocument.h" +#include "nsIDocumentLoaderFactory.h" +#include "nsIDOMWindow.h" +#include "nsIEditingSession.h" +#include "nsIEffectiveTLDService.h" +#include "nsIExternalProtocolService.h" +#include "nsIFormPOSTActionChannel.h" +#include "nsIFrame.h" +#include "nsIGlobalObject.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIIDNService.h" +#include "nsIInputStreamChannel.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsILayoutHistoryState.h" +#include "nsILoadInfo.h" +#include "nsILoadURIDelegate.h" +#include "nsIMultiPartChannel.h" +#include "nsINestedURI.h" +#include "nsINetworkPredictor.h" +#include "nsINode.h" +#include "nsINSSErrorsService.h" +#include "nsIObserverService.h" +#include "nsIOService.h" +#include "nsIPrincipal.h" +#include "nsIPrivacyTransitionObserver.h" +#include "nsIPrompt.h" +#include "nsIPromptCollection.h" +#include "nsIPromptFactory.h" +#include "nsIPublicKeyPinningService.h" +#include "nsIReflowObserver.h" +#include "nsIScriptChannel.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsIScrollableFrame.h" +#include "nsIScrollObserver.h" +#include "nsISupportsPrimitives.h" +#include "nsISecureBrowserUI.h" +#include "nsISeekableStream.h" +#include "nsISelectionDisplay.h" +#include "nsISHEntry.h" +#include "nsISiteSecurityService.h" +#include "nsISocketProvider.h" +#include "nsIStringBundle.h" +#include "nsIStructuredCloneContainer.h" +#include "nsIBrowserChild.h" +#include "nsITextToSubURI.h" +#include "nsITimedChannel.h" +#include "nsITimer.h" +#include "nsITransportSecurityInfo.h" +#include "nsIUploadChannel.h" +#include "nsIURIFixup.h" +#include "nsIURIMutator.h" +#include "nsIURILoader.h" +#include "nsIViewSourceChannel.h" +#include "nsIWebBrowserChrome.h" +#include "nsIWebBrowserChromeFocus.h" +#include "nsIWebBrowserFind.h" +#include "nsIWebProgress.h" +#include "nsIWidget.h" +#include "nsIWindowWatcher.h" +#include "nsIWritablePropertyBag2.h" +#include "nsIX509Cert.h" +#include "nsIXULRuntime.h" + +#include "nsCommandManager.h" +#include "nsPIDOMWindow.h" +#include "nsPIWindowRoot.h" + +#include "IHistory.h" +#include "IUrlClassifierUITelemetry.h" + +#include "nsArray.h" +#include "nsArrayUtils.h" +#include "nsCExternalHandlerService.h" +#include "nsContentDLF.h" +#include "nsContentPolicyUtils.h" // NS_CheckContentLoadPolicy(...) +#include "nsContentSecurityManager.h" +#include "nsContentSecurityUtils.h" +#include "nsContentUtils.h" +#include "nsCURILoader.h" +#include "nsDocShellCID.h" +#include "nsDocShellEditorData.h" +#include "nsDocShellEnumerator.h" +#include "nsDocShellLoadState.h" +#include "nsDocShellLoadTypes.h" +#include "nsDOMCID.h" +#include "nsDOMNavigationTiming.h" +#include "nsDSURIContentListener.h" +#include "nsEditingSession.h" +#include "nsError.h" +#include "nsEscape.h" +#include "nsFocusManager.h" +#include "nsGlobalWindow.h" +#include "nsJSEnvironment.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsObjectLoadingContent.h" +#include "nsPingListener.h" +#include "nsPoint.h" +#include "nsQueryObject.h" +#include "nsQueryActor.h" +#include "nsRect.h" +#include "nsRefreshTimer.h" +#include "nsSandboxFlags.h" +#include "nsSHEntry.h" +#include "nsSHistory.h" +#include "nsSHEntry.h" +#include "nsStructuredCloneContainer.h" +#include "nsSubDocumentFrame.h" +#include "nsURILoader.h" +#include "nsURLHelper.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsViewSourceHandler.h" +#include "nsWebBrowserFind.h" +#include "nsWhitespaceTokenizer.h" +#include "nsWidgetsCID.h" +#include "nsXULAppAPI.h" + +#include "ThirdPartyUtil.h" +#include "GeckoProfiler.h" +#include "mozilla/NullPrincipal.h" +#include "Navigator.h" +#include "prenv.h" +#include "mozilla/ipc/URIUtils.h" +#include "sslerr.h" +#include "mozpkix/pkix.h" +#include "NSSErrorsService.h" + +#include "timeline/JavascriptTimelineMarker.h" +#include "nsDocShellTelemetryUtils.h" + +#ifdef MOZ_PLACES +# include "nsIFaviconService.h" +# include "mozIPlacesPendingOperation.h" +#endif + +#if NS_PRINT_PREVIEW +# include "nsIDocumentViewerPrint.h" +# include "nsIWebBrowserPrint.h" +#endif + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::net; + +using mozilla::ipc::Endpoint; + +// Threshold value in ms for META refresh based redirects +#define REFRESH_REDIRECT_TIMER 15000 + +static mozilla::LazyLogModule gCharsetMenuLog("CharsetMenu"); + +#define LOGCHARSETMENU(args) \ + MOZ_LOG(gCharsetMenuLog, mozilla::LogLevel::Debug, args) + +#ifdef DEBUG +unsigned long nsDocShell::gNumberOfDocShells = 0; +static uint64_t gDocshellIDCounter = 0; + +static mozilla::LazyLogModule gDocShellLog("nsDocShell"); +static mozilla::LazyLogModule gDocShellAndDOMWindowLeakLogging( + "DocShellAndDOMWindowLeak"); +#endif +static mozilla::LazyLogModule gDocShellLeakLog("nsDocShellLeak"); +extern mozilla::LazyLogModule gPageCacheLog; +mozilla::LazyLogModule gSHLog("SessionHistory"); +extern mozilla::LazyLogModule gSHIPBFCacheLog; + +const char kAppstringsBundleURL[] = + "chrome://global/locale/appstrings.properties"; + +static bool IsTopLevelDoc(BrowsingContext* aBrowsingContext, + nsILoadInfo* aLoadInfo) { + MOZ_ASSERT(aBrowsingContext); + MOZ_ASSERT(aLoadInfo); + + if (aLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return false; + } + + return aBrowsingContext->IsTopContent(); +} + +// True if loading for top level document loading in active tab. +static bool IsUrgentStart(BrowsingContext* aBrowsingContext, + nsILoadInfo* aLoadInfo, uint32_t aLoadType) { + MOZ_ASSERT(aBrowsingContext); + MOZ_ASSERT(aLoadInfo); + + if (!IsTopLevelDoc(aBrowsingContext, aLoadInfo)) { + return false; + } + + if (aLoadType & + (nsIDocShell::LOAD_CMD_NORMAL | nsIDocShell::LOAD_CMD_HISTORY)) { + return true; + } + + return aBrowsingContext->IsActive(); +} + +nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, + uint64_t aContentWindowID) + : nsDocLoader(true), + mContentWindowID(aContentWindowID), + mBrowsingContext(aBrowsingContext), + mParentCharset(nullptr), + mTreeOwner(nullptr), + mScrollbarPref(ScrollbarPreference::Auto), + mCharsetReloadState(eCharsetReloadInit), + mParentCharsetSource(0), + mFrameMargins(-1, -1), + mItemType(aBrowsingContext->IsContent() ? typeContent : typeChrome), + mPreviousEntryIndex(-1), + mLoadedEntryIndex(-1), + mBusyFlags(BUSY_FLAGS_NONE), + mAppType(nsIDocShell::APP_TYPE_UNKNOWN), + mLoadType(0), + mFailedLoadType(0), + mJSRunToCompletionDepth(0), + mMetaViewportOverride(nsIDocShell::META_VIEWPORT_OVERRIDE_NONE), + mChannelToDisconnectOnPageHide(0), + mCreatingDocument(false), +#ifdef DEBUG + mInEnsureScriptEnv(false), +#endif + mInitialized(false), + mAllowSubframes(true), + mAllowMetaRedirects(true), + mAllowImages(true), + mAllowMedia(true), + mAllowDNSPrefetch(true), + mAllowWindowControl(true), + mCSSErrorReportingEnabled(false), + mAllowAuth(mItemType == typeContent), + mAllowKeywordFixup(false), + mDisableMetaRefreshWhenInactive(false), + mWindowDraggingAllowed(false), + mInFrameSwap(false), + mFiredUnloadEvent(false), + mEODForCurrentDocument(false), + mURIResultedInDocument(false), + mIsBeingDestroyed(false), + mIsExecutingOnLoadHandler(false), + mSavingOldViewer(false), + mInvisible(false), + mHasLoadedNonBlankURI(false), + mBlankTiming(false), + mTitleValidForCurrentURI(false), + mWillChangeProcess(false), + mIsNavigating(false), + mForcedAutodetection(false), + mCheckingSessionHistory(false), + mNeedToReportActiveAfterLoadingBecomesActive(false) { + // If no outer window ID was provided, generate a new one. + if (aContentWindowID == 0) { + mContentWindowID = nsContentUtils::GenerateWindowId(); + } + + MOZ_LOG(gDocShellLeakLog, LogLevel::Debug, ("DOCSHELL %p created\n", this)); + +#ifdef DEBUG + mDocShellID = gDocshellIDCounter++; + // We're counting the number of |nsDocShells| to help find leaks + ++gNumberOfDocShells; + MOZ_LOG(gDocShellAndDOMWindowLeakLogging, LogLevel::Info, + ("++DOCSHELL %p == %ld [pid = %d] [id = %" PRIu64 "]\n", (void*)this, + gNumberOfDocShells, getpid(), mDocShellID)); +#endif +} + +nsDocShell::~nsDocShell() { + MOZ_ASSERT(!mObserved); + + // Avoid notifying observers while we're in the dtor. + mIsBeingDestroyed = true; + + Destroy(); + + if (mContentViewer) { + mContentViewer->Close(nullptr); + mContentViewer->Destroy(); + mContentViewer = nullptr; + } + + MOZ_LOG(gDocShellLeakLog, LogLevel::Debug, ("DOCSHELL %p destroyed\n", this)); + +#ifdef DEBUG + if (MOZ_LOG_TEST(gDocShellAndDOMWindowLeakLogging, LogLevel::Info)) { + nsAutoCString url; + if (mLastOpenedURI) { + url = mLastOpenedURI->GetSpecOrDefault(); + + // Data URLs can be very long, so truncate to avoid flooding the log. + const uint32_t maxURLLength = 1000; + if (url.Length() > maxURLLength) { + url.Truncate(maxURLLength); + } + } + + // We're counting the number of |nsDocShells| to help find leaks + --gNumberOfDocShells; + MOZ_LOG( + gDocShellAndDOMWindowLeakLogging, LogLevel::Info, + ("--DOCSHELL %p == %ld [pid = %d] [id = %" PRIu64 "] [url = %s]\n", + (void*)this, gNumberOfDocShells, getpid(), mDocShellID, url.get())); + } +#endif +} + +bool nsDocShell::Initialize() { + if (mInitialized) { + // We've already been initialized. + return true; + } + + NS_ASSERTION(mItemType == typeContent || mItemType == typeChrome, + "Unexpected item type in docshell"); + + NS_ENSURE_TRUE(Preferences::GetRootBranch(), false); + mInitialized = true; + + mDisableMetaRefreshWhenInactive = + Preferences::GetBool("browser.meta_refresh_when_inactive.disabled", + mDisableMetaRefreshWhenInactive); + + if (nsCOMPtr<nsIObserverService> serv = services::GetObserverService()) { + const char* msg = mItemType == typeContent ? NS_WEBNAVIGATION_CREATE + : NS_CHROME_WEBNAVIGATION_CREATE; + serv->NotifyWhenScriptSafe(GetAsSupports(this), msg, nullptr); + } + + return true; +} + +/* static */ +already_AddRefed<nsDocShell> nsDocShell::Create( + BrowsingContext* aBrowsingContext, uint64_t aContentWindowID) { + MOZ_ASSERT(aBrowsingContext, "DocShell without a BrowsingContext!"); + + nsresult rv; + RefPtr<nsDocShell> ds = new nsDocShell(aBrowsingContext, aContentWindowID); + + // Initialize the underlying nsDocLoader. + rv = ds->nsDocLoader::InitWithBrowsingContext(aBrowsingContext); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + // Create our ContentListener + ds->mContentListener = new nsDSURIContentListener(ds); + + // We enable if we're in the parent process in order to support non-e10s + // configurations. + // Note: This check is duplicated in SharedWorkerInterfaceRequestor's + // constructor. + if (XRE_IsParentProcess()) { + ds->mInterceptController = new ServiceWorkerInterceptController(); + } + + // We want to hold a strong ref to the loadgroup, so it better hold a weak + // ref to us... use an InterfaceRequestorProxy to do this. + nsCOMPtr<nsIInterfaceRequestor> proxy = new InterfaceRequestorProxy(ds); + ds->mLoadGroup->SetNotificationCallbacks(proxy); + + // XXX(nika): We have our BrowsingContext, so we might be able to skip this. + // It could be nice to directly set up our DocLoader tree? + rv = nsDocLoader::AddDocLoaderAsChildOfRoot(ds); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + // Add |ds| as a progress listener to itself. A little weird, but simpler + // than reproducing all the listener-notification logic in overrides of the + // various methods via which nsDocLoader can be notified. Note that this + // holds an nsWeakPtr to |ds|, so it's ok. + rv = ds->AddProgressListener(ds, nsIWebProgress::NOTIFY_STATE_DOCUMENT | + nsIWebProgress::NOTIFY_STATE_NETWORK | + nsIWebProgress::NOTIFY_LOCATION); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + // If our BrowsingContext has private browsing enabled, update the number of + // private browsing docshells. + if (aBrowsingContext->UsePrivateBrowsing()) { + ds->NotifyPrivateBrowsingChanged(); + } + + // If our parent window is present in this process, set up our parent now. + RefPtr<WindowContext> parentWC = aBrowsingContext->GetParentWindowContext(); + if (parentWC && parentWC->IsInProcess()) { + // If we don't have a parent element anymore, we can't finish this load! + // How'd we get here? + RefPtr<Element> parentElement = aBrowsingContext->GetEmbedderElement(); + if (!parentElement) { + MOZ_ASSERT_UNREACHABLE("nsDocShell::Create() - !parentElement"); + return nullptr; + } + + // We have an in-process parent window, but don't have a parent nsDocShell? + // How'd we get here! + nsCOMPtr<nsIDocShell> parentShell = + parentElement->OwnerDoc()->GetDocShell(); + if (!parentShell) { + MOZ_ASSERT_UNREACHABLE("nsDocShell::Create() - !parentShell"); + return nullptr; + } + parentShell->AddChild(ds); + } + + // Make |ds| the primary DocShell for the given context. + aBrowsingContext->SetDocShell(ds); + + // Set |ds| default load flags on load group. + ds->SetLoadGroupDefaultLoadFlags(aBrowsingContext->GetDefaultLoadFlags()); + + if (XRE_IsParentProcess()) { + aBrowsingContext->Canonical()->MaybeAddAsProgressListener(ds); + } + + return ds.forget(); +} + +void nsDocShell::DestroyChildren() { + for (auto* child : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShellTreeItem> shell = do_QueryObject(child); + NS_ASSERTION(shell, "docshell has null child"); + + if (shell) { + shell->SetTreeOwner(nullptr); + } + } + + nsDocLoader::DestroyChildren(); +} + +NS_IMPL_CYCLE_COLLECTION_WEAK_PTR_INHERITED(nsDocShell, nsDocLoader, + mScriptGlobal, mInitialClientSource, + mBrowsingContext, + mChromeEventHandler) + +NS_IMPL_ADDREF_INHERITED(nsDocShell, nsDocLoader) +NS_IMPL_RELEASE_INHERITED(nsDocShell, nsDocLoader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDocShell) + NS_INTERFACE_MAP_ENTRY(nsIDocShell) + NS_INTERFACE_MAP_ENTRY(nsIDocShellTreeItem) + NS_INTERFACE_MAP_ENTRY(nsIWebNavigation) + NS_INTERFACE_MAP_ENTRY(nsIBaseWindow) + NS_INTERFACE_MAP_ENTRY(nsIRefreshURI) + NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIWebPageDescriptor) + NS_INTERFACE_MAP_ENTRY(nsIAuthPromptProvider) + NS_INTERFACE_MAP_ENTRY(nsILoadContext) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsINetworkInterceptController, + mInterceptController) +NS_INTERFACE_MAP_END_INHERITING(nsDocLoader) + +NS_IMETHODIMP +nsDocShell::GetInterface(const nsIID& aIID, void** aSink) { + MOZ_ASSERT(aSink, "null out param"); + + *aSink = nullptr; + + if (aIID.Equals(NS_GET_IID(nsICommandManager))) { + NS_ENSURE_SUCCESS(EnsureCommandHandler(), NS_ERROR_FAILURE); + *aSink = static_cast<nsICommandManager*>(mCommandManager.get()); + } else if (aIID.Equals(NS_GET_IID(nsIURIContentListener))) { + *aSink = mContentListener; + } else if ((aIID.Equals(NS_GET_IID(nsIScriptGlobalObject)) || + aIID.Equals(NS_GET_IID(nsIGlobalObject)) || + aIID.Equals(NS_GET_IID(nsPIDOMWindowOuter)) || + aIID.Equals(NS_GET_IID(mozIDOMWindowProxy)) || + aIID.Equals(NS_GET_IID(nsIDOMWindow))) && + NS_SUCCEEDED(EnsureScriptEnvironment())) { + return mScriptGlobal->QueryInterface(aIID, aSink); + } else if (aIID.Equals(NS_GET_IID(Document)) && + NS_SUCCEEDED(EnsureContentViewer())) { + RefPtr<Document> doc = mContentViewer->GetDocument(); + doc.forget(aSink); + return *aSink ? NS_OK : NS_NOINTERFACE; + } else if (aIID.Equals(NS_GET_IID(nsIPrompt)) && + NS_SUCCEEDED(EnsureScriptEnvironment())) { + nsresult rv; + nsCOMPtr<nsIWindowWatcher> wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the an auth prompter for our window so that the parenting + // of the dialogs works as it should when using tabs. + nsIPrompt* prompt; + rv = wwatch->GetNewPrompter(mScriptGlobal, &prompt); + NS_ENSURE_SUCCESS(rv, rv); + + *aSink = prompt; + return NS_OK; + } else if (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) || + aIID.Equals(NS_GET_IID(nsIAuthPrompt2))) { + return NS_SUCCEEDED(GetAuthPrompt(PROMPT_NORMAL, aIID, aSink)) + ? NS_OK + : NS_NOINTERFACE; + } else if (aIID.Equals(NS_GET_IID(nsISHistory))) { + // This is deprecated, you should instead directly get + // ChildSHistory from the browsing context. + MOZ_DIAGNOSTIC_ASSERT( + false, "Do not try to get a nsISHistory interface from nsIDocShell"); + return NS_NOINTERFACE; + } else if (aIID.Equals(NS_GET_IID(nsIWebBrowserFind))) { + nsresult rv = EnsureFind(); + if (NS_FAILED(rv)) { + return rv; + } + + *aSink = mFind; + NS_ADDREF((nsISupports*)*aSink); + return NS_OK; + } else if (aIID.Equals(NS_GET_IID(nsISelectionDisplay))) { + if (PresShell* presShell = GetPresShell()) { + return presShell->QueryInterface(aIID, aSink); + } + } else if (aIID.Equals(NS_GET_IID(nsIDocShellTreeOwner))) { + nsCOMPtr<nsIDocShellTreeOwner> treeOwner; + nsresult rv = GetTreeOwner(getter_AddRefs(treeOwner)); + if (NS_SUCCEEDED(rv) && treeOwner) { + return treeOwner->QueryInterface(aIID, aSink); + } + } else if (aIID.Equals(NS_GET_IID(nsIBrowserChild))) { + *aSink = GetBrowserChild().take(); + return *aSink ? NS_OK : NS_ERROR_FAILURE; + } else { + return nsDocLoader::GetInterface(aIID, aSink); + } + + NS_IF_ADDREF(((nsISupports*)*aSink)); + return *aSink ? NS_OK : NS_NOINTERFACE; +} + +NS_IMETHODIMP +nsDocShell::SetCancelContentJSEpoch(int32_t aEpoch) { + // Note: this gets called fairly early (before a pageload actually starts). + // We could probably defer this even longer. + nsCOMPtr<nsIBrowserChild> browserChild = GetBrowserChild(); + static_cast<BrowserChild*>(browserChild.get()) + ->SetCancelContentJSEpoch(aEpoch); + return NS_OK; +} + +nsresult nsDocShell::CheckDisallowedJavascriptLoad( + nsDocShellLoadState* aLoadState) { + if (!net::SchemeIsJavascript(aLoadState->URI())) { + return NS_OK; + } + + if (nsCOMPtr<nsIPrincipal> targetPrincipal = + GetInheritedPrincipal(/* aConsiderCurrentDocument */ true)) { + if (!aLoadState->TriggeringPrincipal()->Subsumes(targetPrincipal)) { + return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI; + } + return NS_OK; + } + return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI; +} + +NS_IMETHODIMP +nsDocShell::LoadURI(nsDocShellLoadState* aLoadState, bool aSetNavigating) { + return LoadURI(aLoadState, aSetNavigating, false); +} + +nsresult nsDocShell::LoadURI(nsDocShellLoadState* aLoadState, + bool aSetNavigating, + bool aContinueHandlingSubframeHistory) { + MOZ_ASSERT(aLoadState, "Must have a valid load state!"); + // NOTE: This comparison between what appears to be internal/external load + // flags is intentional, as it's ensuring that the caller isn't using any of + // the flags reserved for implementations by the `nsIWebNavigation` interface. + // In the future, this check may be dropped. + MOZ_ASSERT( + (aLoadState->LoadFlags() & INTERNAL_LOAD_FLAGS_LOADURI_SETUP_FLAGS) == 0, + "Should not have these flags set"); + MOZ_ASSERT(aLoadState->TargetBrowsingContext().IsNull(), + "Targeting doesn't occur until InternalLoad"); + + if (!aLoadState->TriggeringPrincipal()) { + MOZ_ASSERT(false, "LoadURI must have a triggering principal"); + return NS_ERROR_FAILURE; + } + + MOZ_TRY(CheckDisallowedJavascriptLoad(aLoadState)); + + bool oldIsNavigating = mIsNavigating; + auto cleanupIsNavigating = + MakeScopeExit([&]() { mIsNavigating = oldIsNavigating; }); + if (aSetNavigating) { + mIsNavigating = true; + } + + PopupBlocker::PopupControlState popupState = PopupBlocker::openOverridden; + if (aLoadState->HasLoadFlags(LOAD_FLAGS_ALLOW_POPUPS)) { + popupState = PopupBlocker::openAllowed; + // If we allow popups as part of the navigation, ensure we fake a user + // interaction, so that popups can, in fact, be allowed to open. + if (WindowContext* wc = mBrowsingContext->GetCurrentWindowContext()) { + wc->NotifyUserGestureActivation(); + } + } + + AutoPopupStatePusher statePusher(popupState); + + if (aLoadState->GetCancelContentJSEpoch().isSome()) { + SetCancelContentJSEpoch(*aLoadState->GetCancelContentJSEpoch()); + } + + // Note: we allow loads to get through here even if mFiredUnloadEvent is + // true; that case will get handled in LoadInternal or LoadHistoryEntry, + // so we pass false as the second parameter to IsNavigationAllowed. + // However, we don't allow the page to change location *in the middle of* + // firing beforeunload, so we do need to check if *beforeunload* is currently + // firing, so we call IsNavigationAllowed rather than just IsPrintingOrPP. + if (!IsNavigationAllowed(true, false)) { + return NS_OK; // JS may not handle returning of an error code + } + + nsLoadFlags defaultLoadFlags = mBrowsingContext->GetDefaultLoadFlags(); + if (aLoadState->HasLoadFlags(LOAD_FLAGS_FORCE_TRR)) { + defaultLoadFlags |= nsIRequest::LOAD_TRR_ONLY_MODE; + } else if (aLoadState->HasLoadFlags(LOAD_FLAGS_DISABLE_TRR)) { + defaultLoadFlags |= nsIRequest::LOAD_TRR_DISABLED_MODE; + } + + MOZ_ALWAYS_SUCCEEDS(mBrowsingContext->SetDefaultLoadFlags(defaultLoadFlags)); + + if (!StartupTimeline::HasRecord(StartupTimeline::FIRST_LOAD_URI) && + mItemType == typeContent && !NS_IsAboutBlank(aLoadState->URI())) { + StartupTimeline::RecordOnce(StartupTimeline::FIRST_LOAD_URI); + } + + // LoadType used to be set to a default value here, if no LoadInfo/LoadState + // object was passed in. That functionality has been removed as of bug + // 1492648. LoadType should now be set up by the caller at the time they + // create their nsDocShellLoadState object to pass into LoadURI. + + MOZ_LOG( + gDocShellLeakLog, LogLevel::Debug, + ("nsDocShell[%p]: loading %s with flags 0x%08x", this, + aLoadState->URI()->GetSpecOrDefault().get(), aLoadState->LoadFlags())); + + if ((!aLoadState->LoadIsFromSessionHistory() && + !LOAD_TYPE_HAS_FLAGS(aLoadState->LoadType(), + LOAD_FLAGS_REPLACE_HISTORY)) || + aContinueHandlingSubframeHistory) { + // This is possibly a subframe, so handle it accordingly. + // + // If history exists, it will be loaded into the aLoadState object, and the + // LoadType will be changed. + if (MaybeHandleSubframeHistory(aLoadState, + aContinueHandlingSubframeHistory)) { + // MaybeHandleSubframeHistory returns true if we need to continue loading + // asynchronously. + return NS_OK; + } + } + + if (aLoadState->LoadIsFromSessionHistory()) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell[%p]: loading from session history", this)); + + if (!mozilla::SessionHistoryInParent()) { + nsCOMPtr<nsISHEntry> entry = aLoadState->SHEntry(); + return LoadHistoryEntry(entry, aLoadState->LoadType(), + aLoadState->HasValidUserGestureActivation()); + } + + // FIXME Null check aLoadState->GetLoadingSessionHistoryInfo()? + return LoadHistoryEntry(*aLoadState->GetLoadingSessionHistoryInfo(), + aLoadState->LoadType(), + aLoadState->HasValidUserGestureActivation()); + } + + // On history navigation via Back/Forward buttons, don't execute + // automatic JavaScript redirection such as |location.href = ...| or + // |window.open()| + // + // LOAD_NORMAL: window.open(...) etc. + // LOAD_STOP_CONTENT: location.href = ..., location.assign(...) + if ((aLoadState->LoadType() == LOAD_NORMAL || + aLoadState->LoadType() == LOAD_STOP_CONTENT) && + ShouldBlockLoadingForBackButton()) { + return NS_OK; + } + + BrowsingContext::Type bcType = mBrowsingContext->GetType(); + + // Set up the inheriting principal in LoadState. + nsresult rv = aLoadState->SetupInheritingPrincipal( + bcType, mBrowsingContext->OriginAttributesRef()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aLoadState->SetupTriggeringPrincipal( + mBrowsingContext->OriginAttributesRef()); + NS_ENSURE_SUCCESS(rv, rv); + + aLoadState->CalculateLoadURIFlags(); + + MOZ_ASSERT(aLoadState->TypeHint().IsVoid(), + "Typehint should be null when calling InternalLoad from LoadURI"); + MOZ_ASSERT(aLoadState->FileName().IsVoid(), + "FileName should be null when calling InternalLoad from LoadURI"); + MOZ_ASSERT(!aLoadState->LoadIsFromSessionHistory(), + "Shouldn't be loading from an entry when calling InternalLoad " + "from LoadURI"); + + // If we have a system triggering principal, we can assume that this load was + // triggered by some UI in the browser chrome, such as the URL bar or + // bookmark bar. This should count as a user interaction for the current sh + // entry, so that the user may navigate back to the current entry, from the + // entry that is going to be added as part of this load. + nsCOMPtr<nsIPrincipal> triggeringPrincipal = + aLoadState->TriggeringPrincipal(); + if (triggeringPrincipal && triggeringPrincipal->IsSystemPrincipal()) { + if (mozilla::SessionHistoryInParent()) { + WindowContext* topWc = mBrowsingContext->GetTopWindowContext(); + if (topWc && !topWc->IsDiscarded()) { + MOZ_ALWAYS_SUCCEEDS(topWc->SetSHEntryHasUserInteraction(true)); + } + } else { + bool oshe = false; + nsCOMPtr<nsISHEntry> currentSHEntry; + GetCurrentSHEntry(getter_AddRefs(currentSHEntry), &oshe); + if (currentSHEntry) { + currentSHEntry->SetHasUserInteraction(true); + } + } + } + + rv = InternalLoad(aLoadState); + NS_ENSURE_SUCCESS(rv, rv); + + if (aLoadState->GetOriginalURIString().isSome()) { + // Save URI string in case it's needed later when + // sending to search engine service in EndPageLoad() + mOriginalUriString = *aLoadState->GetOriginalURIString(); + } + + return NS_OK; +} + +bool nsDocShell::IsLoadingFromSessionHistory() { + return mActiveEntryIsLoadingFromSessionHistory; +} + +// StopDetector is modeled similarly to OnloadBlocker; it is a rather +// dummy nsIRequest implementation which can be added to an nsILoadGroup to +// detect Cancel calls. +class StopDetector final : public nsIRequest { + public: + StopDetector() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUEST + + bool Canceled() { return mCanceled; } + + private: + ~StopDetector() = default; + + bool mCanceled = false; +}; + +NS_IMPL_ISUPPORTS(StopDetector, nsIRequest) + +NS_IMETHODIMP +StopDetector::GetName(nsACString& aResult) { + aResult.AssignLiteral("about:stop-detector"); + return NS_OK; +} + +NS_IMETHODIMP +StopDetector::IsPending(bool* aRetVal) { + *aRetVal = true; + return NS_OK; +} + +NS_IMETHODIMP +StopDetector::GetStatus(nsresult* aStatus) { + *aStatus = NS_OK; + return NS_OK; +} + +NS_IMETHODIMP StopDetector::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP StopDetector::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP StopDetector::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP +StopDetector::Cancel(nsresult aStatus) { + mCanceled = true; + return NS_OK; +} + +NS_IMETHODIMP +StopDetector::Suspend(void) { return NS_OK; } +NS_IMETHODIMP +StopDetector::Resume(void) { return NS_OK; } + +NS_IMETHODIMP +StopDetector::GetLoadGroup(nsILoadGroup** aLoadGroup) { + *aLoadGroup = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +StopDetector::SetLoadGroup(nsILoadGroup* aLoadGroup) { return NS_OK; } + +NS_IMETHODIMP +StopDetector::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = nsIRequest::LOAD_NORMAL; + return NS_OK; +} + +NS_IMETHODIMP +StopDetector::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +StopDetector::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +StopDetector::SetLoadFlags(nsLoadFlags aLoadFlags) { return NS_OK; } + +bool nsDocShell::MaybeHandleSubframeHistory( + nsDocShellLoadState* aLoadState, bool aContinueHandlingSubframeHistory) { + // First, verify if this is a subframe. + // Note, it is ok to rely on docshell here and not browsing context since when + // an iframe is created, it has first in-process docshell. + nsCOMPtr<nsIDocShellTreeItem> parentAsItem; + GetInProcessSameTypeParent(getter_AddRefs(parentAsItem)); + nsCOMPtr<nsIDocShell> parentDS(do_QueryInterface(parentAsItem)); + + if (!parentDS || parentDS == static_cast<nsIDocShell*>(this)) { + if (mBrowsingContext && mBrowsingContext->IsTop()) { + // This is the root docshell. If we got here while + // executing an onLoad Handler,this load will not go + // into session history. + // XXX Why is this code in a method which deals with iframes! + bool inOnLoadHandler = false; + GetIsExecutingOnLoadHandler(&inOnLoadHandler); + if (inOnLoadHandler) { + aLoadState->SetLoadType(LOAD_NORMAL_REPLACE); + } + } + return false; + } + + /* OK. It is a subframe. Checkout the parent's loadtype. If the parent was + * loaded through a history mechanism, then get the SH entry for the child + * from the parent. This is done to restore frameset navigation while going + * back/forward. If the parent was loaded through any other loadType, set the + * child's loadType too accordingly, so that session history does not get + * confused. + */ + + // Get the parent's load type + uint32_t parentLoadType; + parentDS->GetLoadType(&parentLoadType); + + if (!aContinueHandlingSubframeHistory) { + if (mozilla::SessionHistoryInParent()) { + if (nsDocShell::Cast(parentDS.get())->IsLoadingFromSessionHistory() && + !GetCreatedDynamically()) { + if (XRE_IsContentProcess()) { + dom::ContentChild* contentChild = dom::ContentChild::GetSingleton(); + nsCOMPtr<nsILoadGroup> loadGroup; + GetLoadGroup(getter_AddRefs(loadGroup)); + if (contentChild && loadGroup && !mCheckingSessionHistory) { + RefPtr<Document> parentDoc = parentDS->GetDocument(); + parentDoc->BlockOnload(); + RefPtr<BrowsingContext> browsingContext = mBrowsingContext; + Maybe<uint64_t> currentLoadIdentifier = + mBrowsingContext->GetCurrentLoadIdentifier(); + RefPtr<nsDocShellLoadState> loadState = aLoadState; + bool isNavigating = mIsNavigating; + RefPtr<StopDetector> stopDetector = new StopDetector(); + loadGroup->AddRequest(stopDetector, nullptr); + // Need to set mCheckingSessionHistory so that + // GetIsAttemptingToNavigate() returns true. + mCheckingSessionHistory = true; + + auto resolve = + [currentLoadIdentifier, browsingContext, parentDoc, loadState, + isNavigating, loadGroup, stopDetector]( + mozilla::Maybe<LoadingSessionHistoryInfo>&& aResult) { + RefPtr<nsDocShell> docShell = + static_cast<nsDocShell*>(browsingContext->GetDocShell()); + auto unblockParent = MakeScopeExit( + [loadGroup, stopDetector, parentDoc, docShell]() { + if (docShell) { + docShell->mCheckingSessionHistory = false; + } + loadGroup->RemoveRequest(stopDetector, nullptr, NS_OK); + parentDoc->UnblockOnload(false); + }); + + if (!docShell || !docShell->mCheckingSessionHistory) { + return; + } + + if (stopDetector->Canceled()) { + return; + } + if (currentLoadIdentifier == + browsingContext->GetCurrentLoadIdentifier() && + aResult.isSome()) { + loadState->SetLoadingSessionHistoryInfo(aResult.value()); + // This is an initial subframe load from the session + // history, index doesn't need to be updated. + loadState->SetLoadIsFromSessionHistory(0, false); + } + + // We got the results back from the parent process, call + // LoadURI again with the possibly updated data. + docShell->LoadURI(loadState, isNavigating, true); + }; + auto reject = [loadGroup, stopDetector, browsingContext, + parentDoc](mozilla::ipc::ResponseRejectReason) { + RefPtr<nsDocShell> docShell = + static_cast<nsDocShell*>(browsingContext->GetDocShell()); + if (docShell) { + docShell->mCheckingSessionHistory = false; + } + // In practise reject shouldn't be called ever. + loadGroup->RemoveRequest(stopDetector, nullptr, NS_OK); + parentDoc->UnblockOnload(false); + }; + contentChild->SendGetLoadingSessionHistoryInfoFromParent( + mBrowsingContext, std::move(resolve), std::move(reject)); + return true; + } + } else { + Maybe<LoadingSessionHistoryInfo> info; + mBrowsingContext->Canonical()->GetLoadingSessionHistoryInfoFromParent( + info); + if (info.isSome()) { + aLoadState->SetLoadingSessionHistoryInfo(info.value()); + // This is an initial subframe load from the session + // history, index doesn't need to be updated. + aLoadState->SetLoadIsFromSessionHistory(0, false); + } + } + } + } else { + // Get the ShEntry for the child from the parent + nsCOMPtr<nsISHEntry> currentSH; + bool oshe = false; + parentDS->GetCurrentSHEntry(getter_AddRefs(currentSH), &oshe); + bool dynamicallyAddedChild = GetCreatedDynamically(); + + if (!dynamicallyAddedChild && !oshe && currentSH) { + // Only use the old SHEntry, if we're sure enough that + // it wasn't originally for some other frame. + nsCOMPtr<nsISHEntry> shEntry; + currentSH->GetChildSHEntryIfHasNoDynamicallyAddedChild( + mBrowsingContext->ChildOffset(), getter_AddRefs(shEntry)); + if (shEntry) { + aLoadState->SetSHEntry(shEntry); + } + } + } + } + + // Make some decisions on the child frame's loadType based on the + // parent's loadType, if the subframe hasn't loaded anything into it. + // + // In some cases privileged scripts may try to get the DOMWindow + // reference of this docshell before the loading starts, causing the + // initial about:blank content viewer being created and mCurrentURI being + // set. To handle this case we check if mCurrentURI is about:blank and + // currentSHEntry is null. + bool oshe = false; + nsCOMPtr<nsISHEntry> currentChildEntry; + GetCurrentSHEntry(getter_AddRefs(currentChildEntry), &oshe); + + if (mCurrentURI && (!NS_IsAboutBlank(mCurrentURI) || currentChildEntry || + mLoadingEntry || mActiveEntry)) { + // This is a pre-existing subframe. If + // 1. The load of this frame was not originally initiated by session + // history directly (i.e. (!shEntry) condition succeeded, but it can + // still be a history load on parent which causes this frame being + // loaded), which we checked with the above assert, and + // 2. mCurrentURI is not null, nor the initial about:blank, + // it is possible that a parent's onLoadHandler or even self's + // onLoadHandler is loading a new page in this child. Check parent's and + // self's busy flag and if it is set, we don't want this onLoadHandler + // load to get in to session history. + BusyFlags parentBusy = parentDS->GetBusyFlags(); + BusyFlags selfBusy = GetBusyFlags(); + + if (parentBusy & BUSY_FLAGS_BUSY || selfBusy & BUSY_FLAGS_BUSY) { + aLoadState->SetLoadType(LOAD_NORMAL_REPLACE); + aLoadState->ClearLoadIsFromSessionHistory(); + } + return false; + } + + // This is a newly created frame. Check for exception cases first. + // By default the subframe will inherit the parent's loadType. + if (aLoadState->LoadIsFromSessionHistory() && + (parentLoadType == LOAD_NORMAL || parentLoadType == LOAD_LINK)) { + // The parent was loaded normally. In this case, this *brand new* + // child really shouldn't have a SHEntry. If it does, it could be + // because the parent is replacing an existing frame with a new frame, + // in the onLoadHandler. We don't want this url to get into session + // history. Clear off shEntry, and set load type to + // LOAD_BYPASS_HISTORY. + bool inOnLoadHandler = false; + parentDS->GetIsExecutingOnLoadHandler(&inOnLoadHandler); + if (inOnLoadHandler) { + aLoadState->SetLoadType(LOAD_NORMAL_REPLACE); + aLoadState->ClearLoadIsFromSessionHistory(); + } + } else if (parentLoadType == LOAD_REFRESH) { + // Clear shEntry. For refresh loads, we have to load + // what comes through the pipe, not what's in history. + aLoadState->ClearLoadIsFromSessionHistory(); + } else if ((parentLoadType == LOAD_BYPASS_HISTORY) || + (aLoadState->LoadIsFromSessionHistory() && + ((parentLoadType & LOAD_CMD_HISTORY) || + (parentLoadType == LOAD_RELOAD_NORMAL) || + (parentLoadType == LOAD_RELOAD_CHARSET_CHANGE) || + (parentLoadType == LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE) || + (parentLoadType == + LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE)))) { + // If the parent url, bypassed history or was loaded from + // history, pass on the parent's loadType to the new child + // frame too, so that the child frame will also + // avoid getting into history. + aLoadState->SetLoadType(parentLoadType); + } else if (parentLoadType == LOAD_ERROR_PAGE) { + // If the parent document is an error page, we don't + // want to update global/session history. However, + // this child frame is not an error page. + aLoadState->SetLoadType(LOAD_BYPASS_HISTORY); + } else if ((parentLoadType == LOAD_RELOAD_BYPASS_CACHE) || + (parentLoadType == LOAD_RELOAD_BYPASS_PROXY) || + (parentLoadType == LOAD_RELOAD_BYPASS_PROXY_AND_CACHE)) { + // the new frame should inherit the parent's load type so that it also + // bypasses the cache and/or proxy + aLoadState->SetLoadType(parentLoadType); + } + + return false; +} + +/* + * Reset state to a new content model within the current document and the + * document viewer. Called by the document before initiating an out of band + * document.write(). + */ +NS_IMETHODIMP +nsDocShell::PrepareForNewContentModel() { + // Clear out our form control state, because the state of controls + // in the pre-open() document should not affect the state of + // controls that are now going to be written. + SetLayoutHistoryState(nullptr); + mEODForCurrentDocument = false; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::FirePageHideNotification(bool aIsUnload) { + FirePageHideNotificationInternal(aIsUnload, false); + return NS_OK; +} + +void nsDocShell::FirePageHideNotificationInternal( + bool aIsUnload, bool aSkipCheckingDynEntries) { + if (mContentViewer && !mFiredUnloadEvent) { + // Keep an explicit reference since calling PageHide could release + // mContentViewer + nsCOMPtr<nsIContentViewer> contentViewer(mContentViewer); + mFiredUnloadEvent = true; + + if (mTiming) { + mTiming->NotifyUnloadEventStart(); + } + + contentViewer->PageHide(aIsUnload); + + if (mTiming) { + mTiming->NotifyUnloadEventEnd(); + } + + AutoTArray<nsCOMPtr<nsIDocShell>, 8> kids; + uint32_t n = mChildList.Length(); + kids.SetCapacity(n); + for (uint32_t i = 0; i < n; i++) { + kids.AppendElement(do_QueryInterface(ChildAt(i))); + } + + n = kids.Length(); + for (uint32_t i = 0; i < n; ++i) { + RefPtr<nsDocShell> child = static_cast<nsDocShell*>(kids[i].get()); + if (child) { + // Skip checking dynamic subframe entries in our children. + child->FirePageHideNotificationInternal(aIsUnload, true); + } + } + + // If the document is unloading, remove all dynamic subframe entries. + if (aIsUnload && !aSkipCheckingDynEntries) { + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (rootSH) { + MOZ_LOG( + gSHLog, LogLevel::Debug, + ("nsDocShell %p unloading, remove dynamic subframe entries", this)); + if (mozilla::SessionHistoryInParent()) { + if (mActiveEntry) { + mBrowsingContext->RemoveDynEntriesFromActiveSessionHistoryEntry(); + } + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p unloading, no active entries", this)); + } else if (mOSHE) { + int32_t index = rootSH->Index(); + rootSH->LegacySHistory()->RemoveDynEntries(index, mOSHE); + } + } + } + + // Now make sure our editor, if any, is detached before we go + // any farther. + DetachEditorFromWindow(); + } +} + +void nsDocShell::ThawFreezeNonRecursive(bool aThaw) { + MOZ_ASSERT(mozilla::BFCacheInParent()); + + if (!mScriptGlobal) { + return; + } + + RefPtr<nsGlobalWindowInner> inner = + mScriptGlobal->GetCurrentInnerWindowInternal(); + if (inner) { + if (aThaw) { + inner->Thaw(false); + } else { + inner->Freeze(false); + } + } +} + +void nsDocShell::FirePageHideShowNonRecursive(bool aShow) { + MOZ_ASSERT(mozilla::BFCacheInParent()); + + if (!mContentViewer) { + return; + } + + // Emulate what non-SHIP BFCache does too. In pageshow case + // add and remove a request and before that call SetCurrentURI to get + // the location change notification. + // For pagehide, set mFiredUnloadEvent to true, so that unload doesn't fire. + nsCOMPtr<nsIContentViewer> contentViewer(mContentViewer); + if (aShow) { + contentViewer->SetIsHidden(false); + mRefreshURIList = std::move(mBFCachedRefreshURIList); + RefreshURIFromQueue(); + mFiredUnloadEvent = false; + RefPtr<Document> doc = contentViewer->GetDocument(); + if (doc) { + doc->NotifyActivityChanged(); + RefPtr<nsGlobalWindowInner> inner = + mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindowInternal() + : nullptr; + if (mBrowsingContext->IsTop()) { + doc->NotifyPossibleTitleChange(false); + doc->SetLoadingOrRestoredFromBFCacheTimeStampToNow(); + if (inner) { + // Now that we have found the inner window of the page restored + // from the history, we have to make sure that + // performance.navigation.type is 2. + // Traditionally this type change has been done to the top level page + // only. + Performance* performance = inner->GetPerformance(); + if (performance) { + performance->GetDOMTiming()->NotifyRestoreStart(); + } + } + } + + nsCOMPtr<nsIChannel> channel = doc->GetChannel(); + if (channel) { + SetLoadType(LOAD_HISTORY); + mEODForCurrentDocument = false; + mIsRestoringDocument = true; + mLoadGroup->AddRequest(channel, nullptr); + SetCurrentURI(doc->GetDocumentURI(), channel, + /* aFireOnLocationChange */ true, + /* aIsInitialAboutBlank */ false, + /* aLocationFlags */ 0); + mLoadGroup->RemoveRequest(channel, nullptr, NS_OK); + mIsRestoringDocument = false; + } + RefPtr<PresShell> presShell = GetPresShell(); + if (presShell) { + presShell->Thaw(false); + } + + if (inner) { + inner->FireDelayedDOMEvents(false); + } + } + } else if (!mFiredUnloadEvent) { + // XXXBFCache check again that the page can enter bfcache. + // XXXBFCache should mTiming->NotifyUnloadEventStart()/End() be called here? + + if (mRefreshURIList) { + RefreshURIToQueue(); + mBFCachedRefreshURIList = std::move(mRefreshURIList); + } else { + // If Stop was called, the list was moved to mSavedRefreshURIList after + // calling SuspendRefreshURIs, which calls RefreshURIToQueue. + mBFCachedRefreshURIList = std::move(mSavedRefreshURIList); + } + + mFiredUnloadEvent = true; + contentViewer->PageHide(false); + + RefPtr<PresShell> presShell = GetPresShell(); + if (presShell) { + presShell->Freeze(false); + } + } +} + +nsresult nsDocShell::Dispatch(TaskCategory aCategory, + already_AddRefed<nsIRunnable>&& aRunnable) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow(); + if (NS_WARN_IF(!win)) { + // Window should only be unavailable after destroyed. + MOZ_ASSERT(mIsBeingDestroyed); + return NS_ERROR_FAILURE; + } + + if (win->GetDocGroup()) { + return win->GetDocGroup()->Dispatch(aCategory, runnable.forget()); + } + + return SchedulerGroup::Dispatch(aCategory, runnable.forget()); +} + +NS_IMETHODIMP +nsDocShell::DispatchLocationChangeEvent() { + return Dispatch( + TaskCategory::Other, + NewRunnableMethod("nsDocShell::FireDummyOnLocationChange", this, + &nsDocShell::FireDummyOnLocationChange)); +} + +NS_IMETHODIMP +nsDocShell::StartDelayedAutoplayMediaComponents() { + RefPtr<nsPIDOMWindowOuter> outerWindow = GetWindow(); + if (outerWindow) { + outerWindow->ActivateMediaComponents(); + } + return NS_OK; +} + +bool nsDocShell::MaybeInitTiming() { + if (mTiming && !mBlankTiming) { + return false; + } + + bool canBeReset = false; + + if (mScriptGlobal && mBlankTiming) { + nsPIDOMWindowInner* innerWin = mScriptGlobal->GetCurrentInnerWindow(); + if (innerWin && innerWin->GetPerformance()) { + mTiming = innerWin->GetPerformance()->GetDOMTiming(); + mBlankTiming = false; + } + } + + if (!mTiming) { + mTiming = new nsDOMNavigationTiming(this); + canBeReset = true; + } + + mTiming->NotifyNavigationStart( + mBrowsingContext->IsActive() + ? nsDOMNavigationTiming::DocShellState::eActive + : nsDOMNavigationTiming::DocShellState::eInactive); + + return canBeReset; +} + +void nsDocShell::MaybeResetInitTiming(bool aReset) { + if (aReset) { + mTiming = nullptr; + } +} + +nsDOMNavigationTiming* nsDocShell::GetNavigationTiming() const { + return mTiming; +} + +nsPresContext* nsDocShell::GetEldestPresContext() { + nsIContentViewer* viewer = mContentViewer; + while (viewer) { + nsIContentViewer* prevViewer = viewer->GetPreviousViewer(); + if (!prevViewer) { + return viewer->GetPresContext(); + } + viewer = prevViewer; + } + + return nullptr; +} + +nsPresContext* nsDocShell::GetPresContext() { + if (!mContentViewer) { + return nullptr; + } + + return mContentViewer->GetPresContext(); +} + +PresShell* nsDocShell::GetPresShell() { + nsPresContext* presContext = GetPresContext(); + return presContext ? presContext->GetPresShell() : nullptr; +} + +PresShell* nsDocShell::GetEldestPresShell() { + nsPresContext* presContext = GetEldestPresContext(); + + if (presContext) { + return presContext->GetPresShell(); + } + + return nullptr; +} + +NS_IMETHODIMP +nsDocShell::GetContentViewer(nsIContentViewer** aContentViewer) { + NS_ENSURE_ARG_POINTER(aContentViewer); + + *aContentViewer = mContentViewer; + NS_IF_ADDREF(*aContentViewer); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetOuterWindowID(uint64_t* aWindowID) { + *aWindowID = mContentWindowID; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetChromeEventHandler(EventTarget* aChromeEventHandler) { + mChromeEventHandler = aChromeEventHandler; + + if (mScriptGlobal) { + mScriptGlobal->SetChromeEventHandler(mChromeEventHandler); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetChromeEventHandler(EventTarget** aChromeEventHandler) { + NS_ENSURE_ARG_POINTER(aChromeEventHandler); + RefPtr<EventTarget> handler = mChromeEventHandler; + handler.forget(aChromeEventHandler); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetCurrentURIForSessionStore(nsIURI* aURI) { + // Note that securityUI will set STATE_IS_INSECURE, even if + // the scheme of |aURI| is "https". + SetCurrentURI(aURI, nullptr, + /* aFireOnLocationChange */ + true, + /* aIsInitialAboutBlank */ + false, + /* aLocationFlags */ + nsIWebProgressListener::LOCATION_CHANGE_SESSION_STORE); + return NS_OK; +} + +bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest, + bool aFireOnLocationChange, + bool aIsInitialAboutBlank, + uint32_t aLocationFlags) { + MOZ_ASSERT(!mIsBeingDestroyed); + + MOZ_LOG(gDocShellLeakLog, LogLevel::Debug, + ("DOCSHELL %p SetCurrentURI %s\n", this, + aURI ? aURI->GetSpecOrDefault().get() : "")); + + // We don't want to send a location change when we're displaying an error + // page, and we don't want to change our idea of "current URI" either + if (mLoadType == LOAD_ERROR_PAGE) { + return false; + } + + bool uriIsEqual = false; + if (!mCurrentURI || !aURI || + NS_FAILED(mCurrentURI->Equals(aURI, &uriIsEqual)) || !uriIsEqual) { + mTitleValidForCurrentURI = false; + } + + mCurrentURI = aURI; + +#ifdef DEBUG + mLastOpenedURI = aURI; +#endif + + if (!NS_IsAboutBlank(mCurrentURI)) { + mHasLoadedNonBlankURI = true; + } + + // Don't fire onLocationChange when creating a subframe's initial about:blank + // document, as this can happen when it's not safe for us to run script. + if (aIsInitialAboutBlank && !mHasLoadedNonBlankURI && + !mBrowsingContext->IsTop()) { + MOZ_ASSERT(!aRequest && aLocationFlags == 0); + return false; + } + + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + if (aFireOnLocationChange) { + FireOnLocationChange(this, aRequest, aURI, aLocationFlags); + } + return !aFireOnLocationChange; +} + +NS_IMETHODIMP +nsDocShell::GetCharset(nsACString& aCharset) { + aCharset.Truncate(); + + PresShell* presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); + Document* doc = presShell->GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + doc->GetDocumentCharacterSet()->Name(aCharset); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::ForceEncodingDetection() { + nsCOMPtr<nsIContentViewer> viewer; + GetContentViewer(getter_AddRefs(viewer)); + if (!viewer) { + return NS_OK; + } + + Document* doc = viewer->GetDocument(); + if (!doc || doc->WillIgnoreCharsetOverride()) { + return NS_OK; + } + + mForcedAutodetection = true; + + nsIURI* url = doc->GetOriginalURI(); + bool isFileURL = url && SchemeIsFile(url); + + int32_t charsetSource = doc->GetDocumentCharacterSetSource(); + auto encoding = doc->GetDocumentCharacterSet(); + // AsHTMLDocument is valid, because we called + // WillIgnoreCharsetOverride() above. + if (doc->AsHTMLDocument()->IsPlainText()) { + switch (charsetSource) { + case kCharsetFromInitialAutoDetectionASCII: + // Deliberately no final version + LOGCHARSETMENU(("TEXT:UnlabeledAscii")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::UnlabeledAscii); + break; + case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Generic: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Generic: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8GenericInitialWasASCII: + case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Content: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Content: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8ContentInitialWasASCII: + LOGCHARSETMENU(("TEXT:UnlabeledNonUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT:: + UnlabeledNonUtf8); + break; + case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLDInitialWasASCII: + LOGCHARSETMENU(("TEXT:UnlabeledNonUtf8TLD")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT:: + UnlabeledNonUtf8TLD); + break; + case kCharsetFromInitialAutoDetectionWouldHaveBeenUTF8: + case kCharsetFromFinalAutoDetectionWouldHaveBeenUTF8InitialWasASCII: + LOGCHARSETMENU(("TEXT:UnlabeledUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::UnlabeledUtf8); + break; + case kCharsetFromChannel: + if (encoding == UTF_8_ENCODING) { + LOGCHARSETMENU(("TEXT:ChannelUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::ChannelUtf8); + } else { + LOGCHARSETMENU(("TEXT:ChannelNonUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT:: + ChannelNonUtf8); + } + break; + default: + LOGCHARSETMENU(("TEXT:Bug")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_TEXT::Bug); + break; + } + } else { + switch (charsetSource) { + case kCharsetFromInitialAutoDetectionASCII: + // Deliberately no final version + LOGCHARSETMENU(("HTML:UnlabeledAscii")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::UnlabeledAscii); + break; + case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Generic: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Generic: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8GenericInitialWasASCII: + case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8Content: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8Content: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8ContentInitialWasASCII: + LOGCHARSETMENU(("HTML:UnlabeledNonUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML:: + UnlabeledNonUtf8); + break; + case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLDInitialWasASCII: + LOGCHARSETMENU(("HTML:UnlabeledNonUtf8TLD")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML:: + UnlabeledNonUtf8TLD); + break; + case kCharsetFromInitialAutoDetectionWouldHaveBeenUTF8: + case kCharsetFromFinalAutoDetectionWouldHaveBeenUTF8InitialWasASCII: + LOGCHARSETMENU(("HTML:UnlabeledUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::UnlabeledUtf8); + break; + case kCharsetFromChannel: + if (encoding == UTF_8_ENCODING) { + LOGCHARSETMENU(("HTML:ChannelUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::ChannelUtf8); + } else { + LOGCHARSETMENU(("HTML:ChannelNonUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML:: + ChannelNonUtf8); + } + break; + case kCharsetFromXmlDeclaration: + case kCharsetFromMetaTag: + if (isFileURL) { + LOGCHARSETMENU(("HTML:LocalLabeled")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::LocalLabeled); + } else if (encoding == UTF_8_ENCODING) { + LOGCHARSETMENU(("HTML:MetaUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::InternalUtf8); + } else { + LOGCHARSETMENU(("HTML:MetaNonUtf8")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML:: + InternalNonUtf8); + } + break; + default: + LOGCHARSETMENU(("HTML:Bug")); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ENCODING_OVERRIDE_SITUATION_HTML::Bug); + break; + } + } + return NS_OK; +} + +void nsDocShell::SetParentCharset(const Encoding*& aCharset, + int32_t aCharsetSource, + nsIPrincipal* aPrincipal) { + mParentCharset = aCharset; + mParentCharsetSource = aCharsetSource; + mParentCharsetPrincipal = aPrincipal; +} + +void nsDocShell::GetParentCharset(const Encoding*& aCharset, + int32_t* aCharsetSource, + nsIPrincipal** aPrincipal) { + aCharset = mParentCharset; + *aCharsetSource = mParentCharsetSource; + NS_IF_ADDREF(*aPrincipal = mParentCharsetPrincipal); +} + +NS_IMETHODIMP +nsDocShell::GetHasTrackingContentBlocked(Promise** aPromise) { + MOZ_ASSERT(aPromise); + + ErrorResult rv; + RefPtr<Document> doc(GetDocument()); + RefPtr<Promise> retPromise = Promise::Create(doc->GetOwnerGlobal(), rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + // Retrieve the document's content blocking events from the parent process. + RefPtr<Document::GetContentBlockingEventsPromise> promise = + doc->GetContentBlockingEvents(); + if (promise) { + promise->Then( + GetCurrentSerialEventTarget(), __func__, + [retPromise](const Document::GetContentBlockingEventsPromise:: + ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + bool has = aValue.ResolveValue() & + nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT; + retPromise->MaybeResolve(has); + } else { + retPromise->MaybeResolve(false); + } + }); + } else { + retPromise->MaybeResolve(false); + } + + retPromise.forget(aPromise); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowPlugins(bool* aAllowPlugins) { + NS_ENSURE_ARG_POINTER(aAllowPlugins); + + *aAllowPlugins = mBrowsingContext->GetAllowPlugins(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowPlugins(bool aAllowPlugins) { + // XXX should enable or disable a plugin host + return mBrowsingContext->SetAllowPlugins(aAllowPlugins); +} + +NS_IMETHODIMP +nsDocShell::GetCssErrorReportingEnabled(bool* aEnabled) { + MOZ_ASSERT(aEnabled); + *aEnabled = mCSSErrorReportingEnabled; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetCssErrorReportingEnabled(bool aEnabled) { + mCSSErrorReportingEnabled = aEnabled; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetUsePrivateBrowsing(bool* aUsePrivateBrowsing) { + NS_ENSURE_ARG_POINTER(aUsePrivateBrowsing); + return mBrowsingContext->GetUsePrivateBrowsing(aUsePrivateBrowsing); +} + +void nsDocShell::NotifyPrivateBrowsingChanged() { + MOZ_ASSERT(!mIsBeingDestroyed); + + nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mPrivacyObservers); + while (iter.HasMore()) { + nsWeakPtr ref = iter.GetNext(); + nsCOMPtr<nsIPrivacyTransitionObserver> obs = do_QueryReferent(ref); + if (!obs) { + iter.Remove(); + } else { + obs->PrivateModeChanged(UsePrivateBrowsing()); + } + } +} + +NS_IMETHODIMP +nsDocShell::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) { + return mBrowsingContext->SetUsePrivateBrowsing(aUsePrivateBrowsing); +} + +NS_IMETHODIMP +nsDocShell::SetPrivateBrowsing(bool aUsePrivateBrowsing) { + return mBrowsingContext->SetPrivateBrowsing(aUsePrivateBrowsing); +} + +NS_IMETHODIMP +nsDocShell::GetHasLoadedNonBlankURI(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = mHasLoadedNonBlankURI; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetUseRemoteTabs(bool* aUseRemoteTabs) { + NS_ENSURE_ARG_POINTER(aUseRemoteTabs); + return mBrowsingContext->GetUseRemoteTabs(aUseRemoteTabs); +} + +NS_IMETHODIMP +nsDocShell::SetRemoteTabs(bool aUseRemoteTabs) { + return mBrowsingContext->SetRemoteTabs(aUseRemoteTabs); +} + +NS_IMETHODIMP +nsDocShell::GetUseRemoteSubframes(bool* aUseRemoteSubframes) { + NS_ENSURE_ARG_POINTER(aUseRemoteSubframes); + return mBrowsingContext->GetUseRemoteSubframes(aUseRemoteSubframes); +} + +NS_IMETHODIMP +nsDocShell::SetRemoteSubframes(bool aUseRemoteSubframes) { + return mBrowsingContext->SetRemoteSubframes(aUseRemoteSubframes); +} + +NS_IMETHODIMP +nsDocShell::AddWeakPrivacyTransitionObserver( + nsIPrivacyTransitionObserver* aObserver) { + nsWeakPtr weakObs = do_GetWeakReference(aObserver); + if (!weakObs) { + return NS_ERROR_NOT_AVAILABLE; + } + mPrivacyObservers.AppendElement(weakObs); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::AddWeakReflowObserver(nsIReflowObserver* aObserver) { + nsWeakPtr weakObs = do_GetWeakReference(aObserver); + if (!weakObs) { + return NS_ERROR_FAILURE; + } + mReflowObservers.AppendElement(weakObs); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::RemoveWeakReflowObserver(nsIReflowObserver* aObserver) { + nsWeakPtr obs = do_GetWeakReference(aObserver); + return mReflowObservers.RemoveElement(obs) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsDocShell::NotifyReflowObservers(bool aInterruptible, + DOMHighResTimeStamp aStart, + DOMHighResTimeStamp aEnd) { + nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mReflowObservers); + while (iter.HasMore()) { + nsWeakPtr ref = iter.GetNext(); + nsCOMPtr<nsIReflowObserver> obs = do_QueryReferent(ref); + if (!obs) { + iter.Remove(); + } else if (aInterruptible) { + obs->ReflowInterruptible(aStart, aEnd); + } else { + obs->Reflow(aStart, aEnd); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowMetaRedirects(bool* aReturn) { + NS_ENSURE_ARG_POINTER(aReturn); + + *aReturn = mAllowMetaRedirects; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowMetaRedirects(bool aValue) { + mAllowMetaRedirects = aValue; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowSubframes(bool* aAllowSubframes) { + NS_ENSURE_ARG_POINTER(aAllowSubframes); + + *aAllowSubframes = mAllowSubframes; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowSubframes(bool aAllowSubframes) { + mAllowSubframes = aAllowSubframes; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowImages(bool* aAllowImages) { + NS_ENSURE_ARG_POINTER(aAllowImages); + + *aAllowImages = mAllowImages; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowImages(bool aAllowImages) { + mAllowImages = aAllowImages; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowMedia(bool* aAllowMedia) { + *aAllowMedia = mAllowMedia; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowMedia(bool aAllowMedia) { + mAllowMedia = aAllowMedia; + + // Mute or unmute audio contexts attached to the inner window. + if (mScriptGlobal) { + if (nsPIDOMWindowInner* innerWin = mScriptGlobal->GetCurrentInnerWindow()) { + if (aAllowMedia) { + innerWin->UnmuteAudioContexts(); + } else { + innerWin->MuteAudioContexts(); + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowDNSPrefetch(bool* aAllowDNSPrefetch) { + *aAllowDNSPrefetch = mAllowDNSPrefetch; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowDNSPrefetch(bool aAllowDNSPrefetch) { + mAllowDNSPrefetch = aAllowDNSPrefetch; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowWindowControl(bool* aAllowWindowControl) { + *aAllowWindowControl = mAllowWindowControl; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowWindowControl(bool aAllowWindowControl) { + mAllowWindowControl = aAllowWindowControl; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowContentRetargeting(bool* aAllowContentRetargeting) { + *aAllowContentRetargeting = mBrowsingContext->GetAllowContentRetargeting(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowContentRetargeting(bool aAllowContentRetargeting) { + BrowsingContext::Transaction txn; + txn.SetAllowContentRetargeting(aAllowContentRetargeting); + txn.SetAllowContentRetargetingOnChildren(aAllowContentRetargeting); + return txn.Commit(mBrowsingContext); +} + +NS_IMETHODIMP +nsDocShell::GetAllowContentRetargetingOnChildren( + bool* aAllowContentRetargetingOnChildren) { + *aAllowContentRetargetingOnChildren = + mBrowsingContext->GetAllowContentRetargetingOnChildren(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowContentRetargetingOnChildren( + bool aAllowContentRetargetingOnChildren) { + return mBrowsingContext->SetAllowContentRetargetingOnChildren( + aAllowContentRetargetingOnChildren); +} + +NS_IMETHODIMP +nsDocShell::GetMayEnableCharacterEncodingMenu( + bool* aMayEnableCharacterEncodingMenu) { + *aMayEnableCharacterEncodingMenu = false; + if (!mContentViewer) { + return NS_OK; + } + Document* doc = mContentViewer->GetDocument(); + if (!doc) { + return NS_OK; + } + if (doc->WillIgnoreCharsetOverride()) { + return NS_OK; + } + + *aMayEnableCharacterEncodingMenu = true; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllDocShellsInSubtree(int32_t aItemType, + DocShellEnumeratorDirection aDirection, + nsTArray<RefPtr<nsIDocShell>>& aResult) { + aResult.Clear(); + + nsDocShellEnumerator docShellEnum( + (aDirection == ENUMERATE_FORWARDS) + ? nsDocShellEnumerator::EnumerationDirection::Forwards + : nsDocShellEnumerator::EnumerationDirection::Backwards, + aItemType, *this); + + nsresult rv = docShellEnum.BuildDocShellArray(aResult); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAppType(AppType* aAppType) { + *aAppType = mAppType; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAppType(AppType aAppType) { + mAppType = aAppType; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetAllowAuth(bool* aAllowAuth) { + *aAllowAuth = mAllowAuth; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetAllowAuth(bool aAllowAuth) { + mAllowAuth = aAllowAuth; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetZoom(float* aZoom) { + NS_ENSURE_ARG_POINTER(aZoom); + *aZoom = 1.0f; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetZoom(float aZoom) { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +nsDocShell::GetBusyFlags(BusyFlags* aBusyFlags) { + NS_ENSURE_ARG_POINTER(aBusyFlags); + + *aBusyFlags = mBusyFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::TabToTreeOwner(bool aForward, bool aForDocumentNavigation, + bool* aTookFocus) { + NS_ENSURE_ARG_POINTER(aTookFocus); + + nsCOMPtr<nsIWebBrowserChromeFocus> chromeFocus = do_GetInterface(mTreeOwner); + if (chromeFocus) { + if (aForward) { + *aTookFocus = + NS_SUCCEEDED(chromeFocus->FocusNextElement(aForDocumentNavigation)); + } else { + *aTookFocus = + NS_SUCCEEDED(chromeFocus->FocusPrevElement(aForDocumentNavigation)); + } + } else { + *aTookFocus = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetLoadURIDelegate(nsILoadURIDelegate** aLoadURIDelegate) { + nsCOMPtr<nsILoadURIDelegate> delegate = GetLoadURIDelegate(); + delegate.forget(aLoadURIDelegate); + return NS_OK; +} + +already_AddRefed<nsILoadURIDelegate> nsDocShell::GetLoadURIDelegate() { + if (nsCOMPtr<nsILoadURIDelegate> result = + do_QueryActor("LoadURIDelegate", GetDocument())) { + return result.forget(); + } + + return nullptr; +} + +NS_IMETHODIMP +nsDocShell::GetUseErrorPages(bool* aUseErrorPages) { + *aUseErrorPages = mBrowsingContext->GetUseErrorPages(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetUseErrorPages(bool aUseErrorPages) { + return mBrowsingContext->SetUseErrorPages(aUseErrorPages); +} + +NS_IMETHODIMP +nsDocShell::GetPreviousEntryIndex(int32_t* aPreviousEntryIndex) { + *aPreviousEntryIndex = mPreviousEntryIndex; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetLoadedEntryIndex(int32_t* aLoadedEntryIndex) { + *aLoadedEntryIndex = mLoadedEntryIndex; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::HistoryPurged(int32_t aNumEntries) { + // These indices are used for fastback cache eviction, to determine + // which session history entries are candidates for content viewer + // eviction. We need to adjust by the number of entries that we + // just purged from history, so that we look at the right session history + // entries during eviction. + mPreviousEntryIndex = std::max(-1, mPreviousEntryIndex - aNumEntries); + mLoadedEntryIndex = std::max(0, mLoadedEntryIndex - aNumEntries); + + for (auto* child : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShell> shell = do_QueryObject(child); + if (shell) { + shell->HistoryPurged(aNumEntries); + } + } + + return NS_OK; +} + +void nsDocShell::TriggerParentCheckDocShellIsEmpty() { + if (RefPtr<nsDocShell> parent = GetInProcessParentDocshell()) { + parent->DocLoaderIsEmpty(true); + } + if (GetBrowsingContext()->IsContentSubframe() && + !GetBrowsingContext()->GetParent()->IsInProcess()) { + if (BrowserChild* browserChild = BrowserChild::GetFrom(this)) { + mozilla::Unused << browserChild->SendMaybeFireEmbedderLoadEvents( + EmbedderElementEventType::NoEvent); + } + } +} + +nsresult nsDocShell::HistoryEntryRemoved(int32_t aIndex) { + // These indices are used for fastback cache eviction, to determine + // which session history entries are candidates for content viewer + // eviction. We need to adjust by the number of entries that we + // just purged from history, so that we look at the right session history + // entries during eviction. + if (aIndex == mPreviousEntryIndex) { + mPreviousEntryIndex = -1; + } else if (aIndex < mPreviousEntryIndex) { + --mPreviousEntryIndex; + } + if (mLoadedEntryIndex == aIndex) { + mLoadedEntryIndex = 0; + } else if (aIndex < mLoadedEntryIndex) { + --mLoadedEntryIndex; + } + + for (auto* child : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShell> shell = do_QueryObject(child); + if (shell) { + static_cast<nsDocShell*>(shell.get())->HistoryEntryRemoved(aIndex); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetRecordProfileTimelineMarkers(bool aValue) { + bool currentValue = nsIDocShell::GetRecordProfileTimelineMarkers(); + if (currentValue == aValue) { + return NS_OK; + } + + if (aValue) { + MOZ_ASSERT(!TimelineConsumers::HasConsumer(this)); + TimelineConsumers::AddConsumer(this); + MOZ_ASSERT(TimelineConsumers::HasConsumer(this)); + UseEntryScriptProfiling(); + } else { + MOZ_ASSERT(TimelineConsumers::HasConsumer(this)); + TimelineConsumers::RemoveConsumer(this); + MOZ_ASSERT(!TimelineConsumers::HasConsumer(this)); + UnuseEntryScriptProfiling(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetRecordProfileTimelineMarkers(bool* aValue) { + *aValue = !!mObserved; + return NS_OK; +} + +nsresult nsDocShell::PopProfileTimelineMarkers( + JSContext* aCx, JS::MutableHandle<JS::Value> aOut) { + nsTArray<dom::ProfileTimelineMarker> store; + SequenceRooter<dom::ProfileTimelineMarker> rooter(aCx, &store); + + TimelineConsumers::PopMarkers(this, aCx, store); + + if (!ToJSValue(aCx, store, aOut)) { + JS_ClearPendingException(aCx); + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +nsresult nsDocShell::Now(DOMHighResTimeStamp* aWhen) { + *aWhen = (TimeStamp::Now() - TimeStamp::ProcessCreation()).ToMilliseconds(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetWindowDraggingAllowed(bool aValue) { + RefPtr<nsDocShell> parent = GetInProcessParentDocshell(); + if (!aValue && mItemType == typeChrome && !parent) { + // Window dragging is always allowed for top level + // chrome docshells. + return NS_ERROR_FAILURE; + } + mWindowDraggingAllowed = aValue; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetWindowDraggingAllowed(bool* aValue) { + // window dragging regions in CSS (-moz-window-drag:drag) + // can be slow. Default behavior is to only allow it for + // chrome top level windows. + RefPtr<nsDocShell> parent = GetInProcessParentDocshell(); + if (mItemType == typeChrome && !parent) { + // Top level chrome window + *aValue = true; + } else { + *aValue = mWindowDraggingAllowed; + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetCurrentDocumentChannel(nsIChannel** aResult) { + NS_IF_ADDREF(*aResult = GetCurrentDocChannel()); + return NS_OK; +} + +nsIChannel* nsDocShell::GetCurrentDocChannel() { + if (mContentViewer) { + Document* doc = mContentViewer->GetDocument(); + if (doc) { + return doc->GetChannel(); + } + } + return nullptr; +} + +NS_IMETHODIMP +nsDocShell::AddWeakScrollObserver(nsIScrollObserver* aObserver) { + nsWeakPtr weakObs = do_GetWeakReference(aObserver); + if (!weakObs) { + return NS_ERROR_FAILURE; + } + mScrollObservers.AppendElement(weakObs); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::RemoveWeakScrollObserver(nsIScrollObserver* aObserver) { + nsWeakPtr obs = do_GetWeakReference(aObserver); + return mScrollObservers.RemoveElement(obs) ? NS_OK : NS_ERROR_FAILURE; +} + +void nsDocShell::NotifyAsyncPanZoomStarted() { + nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mScrollObservers); + while (iter.HasMore()) { + nsWeakPtr ref = iter.GetNext(); + nsCOMPtr<nsIScrollObserver> obs = do_QueryReferent(ref); + if (obs) { + obs->AsyncPanZoomStarted(); + } else { + iter.Remove(); + } + } +} + +void nsDocShell::NotifyAsyncPanZoomStopped() { + nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mScrollObservers); + while (iter.HasMore()) { + nsWeakPtr ref = iter.GetNext(); + nsCOMPtr<nsIScrollObserver> obs = do_QueryReferent(ref); + if (obs) { + obs->AsyncPanZoomStopped(); + } else { + iter.Remove(); + } + } +} + +NS_IMETHODIMP +nsDocShell::NotifyScrollObservers() { + nsTObserverArray<nsWeakPtr>::ForwardIterator iter(mScrollObservers); + while (iter.HasMore()) { + nsWeakPtr ref = iter.GetNext(); + nsCOMPtr<nsIScrollObserver> obs = do_QueryReferent(ref); + if (obs) { + obs->ScrollPositionChanged(); + } else { + iter.Remove(); + } + } + return NS_OK; +} + +//***************************************************************************** +// nsDocShell::nsIDocShellTreeItem +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::GetName(nsAString& aName) { + aName = mBrowsingContext->Name(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetName(const nsAString& aName) { + return mBrowsingContext->SetName(aName); +} + +NS_IMETHODIMP +nsDocShell::NameEquals(const nsAString& aName, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = mBrowsingContext->NameEquals(aName); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetCustomUserAgent(nsAString& aCustomUserAgent) { + mBrowsingContext->GetCustomUserAgent(aCustomUserAgent); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetCustomUserAgent(const nsAString& aCustomUserAgent) { + if (mWillChangeProcess) { + NS_WARNING("SetCustomUserAgent: Process is changing. Ignoring set"); + return NS_ERROR_FAILURE; + } + + return mBrowsingContext->SetCustomUserAgent(aCustomUserAgent); +} + +NS_IMETHODIMP +nsDocShell::ClearCachedPlatform() { + RefPtr<nsGlobalWindowInner> win = + mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindowInternal() : nullptr; + if (win) { + Navigator* navigator = win->Navigator(); + if (navigator) { + navigator->ClearPlatformCache(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::ClearCachedUserAgent() { + RefPtr<nsGlobalWindowInner> win = + mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindowInternal() : nullptr; + if (win) { + Navigator* navigator = win->Navigator(); + if (navigator) { + navigator->ClearUserAgentCache(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetMetaViewportOverride( + MetaViewportOverride* aMetaViewportOverride) { + NS_ENSURE_ARG_POINTER(aMetaViewportOverride); + + *aMetaViewportOverride = mMetaViewportOverride; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetMetaViewportOverride( + MetaViewportOverride aMetaViewportOverride) { + // We don't have a way to verify this coming from Javascript, so this check is + // still needed. + if (!(aMetaViewportOverride == META_VIEWPORT_OVERRIDE_NONE || + aMetaViewportOverride == META_VIEWPORT_OVERRIDE_ENABLED || + aMetaViewportOverride == META_VIEWPORT_OVERRIDE_DISABLED)) { + return NS_ERROR_INVALID_ARG; + } + + mMetaViewportOverride = aMetaViewportOverride; + + // Inform our presShell that it needs to re-check its need for a viewport + // override. + if (RefPtr<PresShell> presShell = GetPresShell()) { + presShell->MaybeRecreateMobileViewportManager(true); + } + + return NS_OK; +} + +/* virtual */ +int32_t nsDocShell::ItemType() { return mItemType; } + +NS_IMETHODIMP +nsDocShell::GetItemType(int32_t* aItemType) { + NS_ENSURE_ARG_POINTER(aItemType); + + MOZ_DIAGNOSTIC_ASSERT( + (mBrowsingContext->IsContent() ? typeContent : typeChrome) == mItemType); + *aItemType = mItemType; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetInProcessParent(nsIDocShellTreeItem** aParent) { + if (!mParent) { + *aParent = nullptr; + } else { + CallQueryInterface(mParent, aParent); + } + // Note that in the case when the parent is not an nsIDocShellTreeItem we + // don't want to throw; we just want to return null. + return NS_OK; +} + +// With Fission, related nsDocShell objects may exist in a different process. In +// that case, this method will return `nullptr`, despite a parent nsDocShell +// object existing. +// +// Prefer using `BrowsingContext::Parent()`, which will succeed even if the +// parent entry is not in the current process, and handle the case where the +// parent nsDocShell is inaccessible. +already_AddRefed<nsDocShell> nsDocShell::GetInProcessParentDocshell() { + nsCOMPtr<nsIDocShell> docshell = do_QueryInterface(GetAsSupports(mParent)); + return docshell.forget().downcast<nsDocShell>(); +} + +void nsDocShell::MaybeCreateInitialClientSource(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(!mIsBeingDestroyed); + + // If there is an existing document then there is no need to create + // a client for a future initial about:blank document. + if (mScriptGlobal && mScriptGlobal->GetCurrentInnerWindowInternal() && + mScriptGlobal->GetCurrentInnerWindowInternal()->GetExtantDoc()) { + MOZ_DIAGNOSTIC_ASSERT(mScriptGlobal->GetCurrentInnerWindowInternal() + ->GetClientInfo() + .isSome()); + MOZ_DIAGNOSTIC_ASSERT(!mInitialClientSource); + return; + } + + // Don't recreate the initial client source. We call this multiple times + // when DoChannelLoad() is called before CreateAboutBlankContentViewer. + if (mInitialClientSource) { + return; + } + + // Don't pre-allocate the client when we are sandboxed. The inherited + // principal does not take sandboxing into account. + // TODO: Refactor sandboxing principal code out so we can use it here. + if (!aPrincipal && mBrowsingContext->GetSandboxFlags()) { + return; + } + + // We cannot get inherited foreign partitioned principal here. Instead, we + // directly check which principal we want to inherit for the service worker. + nsIPrincipal* principal = + aPrincipal + ? aPrincipal + : GetInheritedPrincipal( + false, StoragePrincipalHelper:: + ShouldUsePartitionPrincipalForServiceWorker(this)); + + // Sometimes there is no principal available when we are called from + // CreateAboutBlankContentViewer. For example, sometimes the principal + // is only extracted from the load context after the document is created + // in Document::ResetToURI(). Ideally we would do something similar + // here, but for now lets just avoid the issue by not preallocating the + // client. + if (!principal) { + return; + } + + nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow(); + if (!win) { + return; + } + + mInitialClientSource = ClientManager::CreateSource( + ClientType::Window, win->EventTargetFor(TaskCategory::Other), principal); + MOZ_DIAGNOSTIC_ASSERT(mInitialClientSource); + + // Mark the initial client as execution ready, but owned by the docshell. + // If the client is actually used this will cause ClientSource to force + // the creation of the initial about:blank by calling + // nsDocShell::GetDocument(). + mInitialClientSource->DocShellExecutionReady(this); + + // Next, check to see if the parent is controlled. + nsCOMPtr<nsIDocShell> parent = GetInProcessParentDocshell(); + nsPIDOMWindowOuter* parentOuter = parent ? parent->GetWindow() : nullptr; + nsPIDOMWindowInner* parentInner = + parentOuter ? parentOuter->GetCurrentInnerWindow() : nullptr; + if (!parentInner) { + return; + } + + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), "about:blank"_ns)); + + // We're done if there is no parent controller or if this docshell + // is not permitted to control for some reason. + Maybe<ServiceWorkerDescriptor> controller(parentInner->GetController()); + if (controller.isNothing() || + !ServiceWorkerAllowedToControlWindow(principal, uri)) { + return; + } + + mInitialClientSource->InheritController(controller.ref()); +} + +Maybe<ClientInfo> nsDocShell::GetInitialClientInfo() const { + if (mInitialClientSource) { + Maybe<ClientInfo> result; + result.emplace(mInitialClientSource->Info()); + return result; + } + + nsGlobalWindowInner* innerWindow = + mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindowInternal() : nullptr; + Document* doc = innerWindow ? innerWindow->GetExtantDoc() : nullptr; + + if (!doc || !doc->IsInitialDocument()) { + return Maybe<ClientInfo>(); + } + + return innerWindow->GetClientInfo(); +} + +nsresult nsDocShell::SetDocLoaderParent(nsDocLoader* aParent) { + bool wasFrame = IsSubframe(); + + nsresult rv = nsDocLoader::SetDocLoaderParent(aParent); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsPriority> priorityGroup = do_QueryInterface(mLoadGroup); + if (wasFrame != IsSubframe() && priorityGroup) { + priorityGroup->AdjustPriority(wasFrame ? -1 : 1); + } + + // Curse ambiguous nsISupports inheritance! + nsISupports* parent = GetAsSupports(aParent); + + // If parent is another docshell, we inherit all their flags for + // allowing plugins, scripting etc. + bool value; + nsCOMPtr<nsIDocShell> parentAsDocShell(do_QueryInterface(parent)); + + if (parentAsDocShell) { + if (mAllowMetaRedirects && + NS_SUCCEEDED(parentAsDocShell->GetAllowMetaRedirects(&value))) { + SetAllowMetaRedirects(value); + } + if (mAllowSubframes && + NS_SUCCEEDED(parentAsDocShell->GetAllowSubframes(&value))) { + SetAllowSubframes(value); + } + if (mAllowImages && + NS_SUCCEEDED(parentAsDocShell->GetAllowImages(&value))) { + SetAllowImages(value); + } + SetAllowMedia(parentAsDocShell->GetAllowMedia() && mAllowMedia); + if (mAllowWindowControl && + NS_SUCCEEDED(parentAsDocShell->GetAllowWindowControl(&value))) { + SetAllowWindowControl(value); + } + if (NS_FAILED(parentAsDocShell->GetAllowDNSPrefetch(&value))) { + value = false; + } + SetAllowDNSPrefetch(mAllowDNSPrefetch && value); + + // We don't need to inherit metaViewportOverride, because the viewport + // is only relevant for the outermost nsDocShell, not for any iframes + // like this that might be embedded within it. + } + + nsCOMPtr<nsIURIContentListener> parentURIListener(do_GetInterface(parent)); + if (parentURIListener) { + mContentListener->SetParentContentListener(parentURIListener); + } + + return NS_OK; +} + +void nsDocShell::MaybeRestoreWindowName() { + if (!StaticPrefs::privacy_window_name_update_enabled()) { + return; + } + + // We only restore window.name for the top-level content. + if (!mBrowsingContext->IsTopContent()) { + return; + } + + nsAutoString name; + + // Following implements https://html.spec.whatwg.org/#history-traversal: + // Step 4.4. Check if the loading entry has a name. + + if (mLSHE) { + mLSHE->GetName(name); + } + + if (mLoadingEntry) { + name = mLoadingEntry->mInfo.GetName(); + } + + if (name.IsEmpty()) { + return; + } + + // Step 4.4.1. Set the name to the browsing context. + Unused << mBrowsingContext->SetName(name); + + // Step 4.4.2. Clear the name of all entries that are contiguous and + // same-origin with the loading entry. + if (mLSHE) { + nsSHistory::WalkContiguousEntries( + mLSHE, [](nsISHEntry* aEntry) { aEntry->SetName(EmptyString()); }); + } + + if (mLoadingEntry) { + // Clear the name of the session entry in the child side. For parent side, + // the clearing will be done when we commit the history to the parent. + mLoadingEntry->mInfo.SetName(EmptyString()); + } +} + +void nsDocShell::StoreWindowNameToSHEntries() { + MOZ_ASSERT(mBrowsingContext->IsTopContent()); + + nsAutoString name; + mBrowsingContext->GetName(name); + + if (mOSHE) { + nsSHistory::WalkContiguousEntries( + mOSHE, [&](nsISHEntry* aEntry) { aEntry->SetName(name); }); + } + + if (mozilla::SessionHistoryInParent()) { + if (XRE_IsParentProcess()) { + SessionHistoryEntry* entry = + mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry(); + if (entry) { + nsSHistory::WalkContiguousEntries( + entry, [&](nsISHEntry* aEntry) { aEntry->SetName(name); }); + } + } else { + // Ask parent process to store the name in entries. + mozilla::Unused + << ContentChild::GetSingleton() + ->SendSessionHistoryEntryStoreWindowNameInContiguousEntries( + mBrowsingContext, name); + } + } +} + +NS_IMETHODIMP +nsDocShell::GetInProcessSameTypeParent(nsIDocShellTreeItem** aParent) { + if (BrowsingContext* parentBC = mBrowsingContext->GetParent()) { + *aParent = do_AddRef(parentBC->GetDocShell()).take(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetSameTypeInProcessParentIgnoreBrowserBoundaries( + nsIDocShell** aParent) { + NS_ENSURE_ARG_POINTER(aParent); + *aParent = nullptr; + + nsCOMPtr<nsIDocShellTreeItem> parent = + do_QueryInterface(GetAsSupports(mParent)); + if (!parent) { + return NS_OK; + } + + if (parent->ItemType() == mItemType) { + nsCOMPtr<nsIDocShell> parentDS = do_QueryInterface(parent); + parentDS.forget(aParent); + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetInProcessRootTreeItem(nsIDocShellTreeItem** aRootTreeItem) { + NS_ENSURE_ARG_POINTER(aRootTreeItem); + + RefPtr<nsDocShell> root = this; + RefPtr<nsDocShell> parent = root->GetInProcessParentDocshell(); + while (parent) { + root = parent; + parent = root->GetInProcessParentDocshell(); + } + + root.forget(aRootTreeItem); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetInProcessSameTypeRootTreeItem( + nsIDocShellTreeItem** aRootTreeItem) { + NS_ENSURE_ARG_POINTER(aRootTreeItem); + *aRootTreeItem = static_cast<nsIDocShellTreeItem*>(this); + + nsCOMPtr<nsIDocShellTreeItem> parent; + NS_ENSURE_SUCCESS(GetInProcessSameTypeParent(getter_AddRefs(parent)), + NS_ERROR_FAILURE); + while (parent) { + *aRootTreeItem = parent; + NS_ENSURE_SUCCESS( + (*aRootTreeItem)->GetInProcessSameTypeParent(getter_AddRefs(parent)), + NS_ERROR_FAILURE); + } + NS_ADDREF(*aRootTreeItem); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetTreeOwner(nsIDocShellTreeOwner** aTreeOwner) { + NS_ENSURE_ARG_POINTER(aTreeOwner); + + *aTreeOwner = mTreeOwner; + NS_IF_ADDREF(*aTreeOwner); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetTreeOwner(nsIDocShellTreeOwner* aTreeOwner) { + if (mIsBeingDestroyed && aTreeOwner) { + return NS_ERROR_FAILURE; + } + + // Don't automatically set the progress based on the tree owner for frames + if (!IsSubframe()) { + nsCOMPtr<nsIWebProgress> webProgress = + do_QueryInterface(GetAsSupports(this)); + + if (webProgress) { + nsCOMPtr<nsIWebProgressListener> oldListener = + do_QueryInterface(mTreeOwner); + nsCOMPtr<nsIWebProgressListener> newListener = + do_QueryInterface(aTreeOwner); + + if (oldListener) { + webProgress->RemoveProgressListener(oldListener); + } + + if (newListener) { + webProgress->AddProgressListener(newListener, + nsIWebProgress::NOTIFY_ALL); + } + } + } + + mTreeOwner = aTreeOwner; // Weak reference per API + + for (auto* childDocLoader : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShellTreeItem> child = do_QueryObject(childDocLoader); + NS_ENSURE_TRUE(child, NS_ERROR_FAILURE); + + if (child->ItemType() == mItemType) { + child->SetTreeOwner(aTreeOwner); + } + } + + // If we're in the content process and have had a TreeOwner set on us, extract + // our BrowserChild actor. If we've already had our BrowserChild set, assert + // that it hasn't changed. + if (mTreeOwner && XRE_IsContentProcess()) { + nsCOMPtr<nsIBrowserChild> newBrowserChild = do_GetInterface(mTreeOwner); + MOZ_ASSERT(newBrowserChild, + "No BrowserChild actor for tree owner in Content!"); + + if (mBrowserChild) { + nsCOMPtr<nsIBrowserChild> oldBrowserChild = + do_QueryReferent(mBrowserChild); + MOZ_RELEASE_ASSERT( + oldBrowserChild == newBrowserChild, + "Cannot change BrowserChild during nsDocShell lifetime!"); + } else { + mBrowserChild = do_GetWeakReference(newBrowserChild); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetHistoryID(nsID& aID) { + aID = mBrowsingContext->GetHistoryID(); + return NS_OK; +} + +const nsID& nsDocShell::HistoryID() { return mBrowsingContext->GetHistoryID(); } + +NS_IMETHODIMP +nsDocShell::GetIsInUnload(bool* aIsInUnload) { + *aIsInUnload = mFiredUnloadEvent; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetInProcessChildCount(int32_t* aChildCount) { + NS_ENSURE_ARG_POINTER(aChildCount); + *aChildCount = mChildList.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::AddChild(nsIDocShellTreeItem* aChild) { + NS_ENSURE_ARG_POINTER(aChild); + + RefPtr<nsDocLoader> childAsDocLoader = GetAsDocLoader(aChild); + NS_ENSURE_TRUE(childAsDocLoader, NS_ERROR_UNEXPECTED); + + // Make sure we're not creating a loop in the docshell tree + nsDocLoader* ancestor = this; + do { + if (childAsDocLoader == ancestor) { + return NS_ERROR_ILLEGAL_VALUE; + } + ancestor = ancestor->GetParent(); + } while (ancestor); + + // Make sure to remove the child from its current parent. + nsDocLoader* childsParent = childAsDocLoader->GetParent(); + if (childsParent) { + nsresult rv = childsParent->RemoveChildLoader(childAsDocLoader); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Make sure to clear the treeowner in case this child is a different type + // from us. + aChild->SetTreeOwner(nullptr); + + nsresult res = AddChildLoader(childAsDocLoader); + NS_ENSURE_SUCCESS(res, res); + NS_ASSERTION(!mChildList.IsEmpty(), + "child list must not be empty after a successful add"); + + /* Set the child's global history if the parent has one */ + if (mBrowsingContext->GetUseGlobalHistory()) { + // childDocShell->SetUseGlobalHistory(true); + // this should be set through BC inherit + MOZ_ASSERT(aChild->GetBrowsingContext()->GetUseGlobalHistory()); + } + + if (aChild->ItemType() != mItemType) { + return NS_OK; + } + + aChild->SetTreeOwner(mTreeOwner); + + nsCOMPtr<nsIDocShell> childAsDocShell(do_QueryInterface(aChild)); + if (!childAsDocShell) { + return NS_OK; + } + + // charset, style-disabling, and zoom will be inherited in SetupNewViewer() + + // Now take this document's charset and set the child's parentCharset field + // to it. We'll later use that field, in the loading process, for the + // charset choosing algorithm. + // If we fail, at any point, we just return NS_OK. + // This code has some performance impact. But this will be reduced when + // the current charset will finally be stored as an Atom, avoiding the + // alias resolution extra look-up. + + // we are NOT going to propagate the charset is this Chrome's docshell + if (mItemType == nsIDocShellTreeItem::typeChrome) { + return NS_OK; + } + + // get the parent's current charset + if (!mContentViewer) { + return NS_OK; + } + Document* doc = mContentViewer->GetDocument(); + if (!doc) { + return NS_OK; + } + + const Encoding* parentCS = doc->GetDocumentCharacterSet(); + int32_t charsetSource = doc->GetDocumentCharacterSetSource(); + // set the child's parentCharset + childAsDocShell->SetParentCharset(parentCS, charsetSource, + doc->NodePrincipal()); + + // printf("### 1 >>> Adding child. Parent CS = %s. ItemType = %d.\n", + // NS_LossyConvertUTF16toASCII(parentCS).get(), mItemType); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::RemoveChild(nsIDocShellTreeItem* aChild) { + NS_ENSURE_ARG_POINTER(aChild); + + RefPtr<nsDocLoader> childAsDocLoader = GetAsDocLoader(aChild); + NS_ENSURE_TRUE(childAsDocLoader, NS_ERROR_UNEXPECTED); + + nsresult rv = RemoveChildLoader(childAsDocLoader); + NS_ENSURE_SUCCESS(rv, rv); + + aChild->SetTreeOwner(nullptr); + + return nsDocLoader::AddDocLoaderAsChildOfRoot(childAsDocLoader); +} + +NS_IMETHODIMP +nsDocShell::GetInProcessChildAt(int32_t aIndex, nsIDocShellTreeItem** aChild) { + NS_ENSURE_ARG_POINTER(aChild); + + RefPtr<nsDocShell> child = GetInProcessChildAt(aIndex); + NS_ENSURE_TRUE(child, NS_ERROR_UNEXPECTED); + + child.forget(aChild); + + return NS_OK; +} + +nsDocShell* nsDocShell::GetInProcessChildAt(int32_t aIndex) { +#ifdef DEBUG + if (aIndex < 0) { + NS_WARNING("Negative index passed to GetChildAt"); + } else if (static_cast<uint32_t>(aIndex) >= mChildList.Length()) { + NS_WARNING("Too large an index passed to GetChildAt"); + } +#endif + + nsIDocumentLoader* child = ChildAt(aIndex); + + // child may be nullptr here. + return static_cast<nsDocShell*>(child); +} + +nsresult nsDocShell::AddChildSHEntry(nsISHEntry* aCloneRef, + nsISHEntry* aNewEntry, + int32_t aChildOffset, uint32_t aLoadType, + bool aCloneChildren) { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + nsresult rv = NS_OK; + + if (mLSHE && aLoadType != LOAD_PUSHSTATE) { + /* You get here if you are currently building a + * hierarchy ie.,you just visited a frameset page + */ + if (NS_FAILED(mLSHE->ReplaceChild(aNewEntry))) { + rv = mLSHE->AddChild(aNewEntry, aChildOffset); + } + } else if (!aCloneRef) { + /* This is an initial load in some subframe. Just append it if we can */ + if (mOSHE) { + rv = mOSHE->AddChild(aNewEntry, aChildOffset, UseRemoteSubframes()); + } + } else { + RefPtr<ChildSHistory> shistory = GetRootSessionHistory(); + if (shistory) { + rv = shistory->LegacySHistory()->AddChildSHEntryHelper( + aCloneRef, aNewEntry, mBrowsingContext->Top(), aCloneChildren); + } + } + return rv; +} + +nsresult nsDocShell::AddChildSHEntryToParent(nsISHEntry* aNewEntry, + int32_t aChildOffset, + bool aCloneChildren) { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + /* You will get here when you are in a subframe and + * a new url has been loaded on you. + * The mOSHE in this subframe will be the previous url's + * mOSHE. This mOSHE will be used as the identification + * for this subframe in the CloneAndReplace function. + */ + + // In this case, we will end up calling AddEntry, which increases the + // current index by 1 + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (rootSH) { + mPreviousEntryIndex = rootSH->Index(); + } + + nsresult rv; + // XXX(farre): this is not Fission safe, expect errors. This never + // get's executed once session history in the parent is enabled. + nsCOMPtr<nsIDocShell> parent = do_QueryInterface(GetAsSupports(mParent), &rv); + NS_WARNING_ASSERTION( + parent || !UseRemoteSubframes(), + "Failed to add child session history entry! This will be resolved once " + "session history in the parent is enabled."); + if (parent) { + rv = nsDocShell::Cast(parent)->AddChildSHEntry( + mOSHE, aNewEntry, aChildOffset, mLoadType, aCloneChildren); + } + + if (rootSH) { + mLoadedEntryIndex = rootSH->Index(); + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gPageCacheLog, LogLevel::Verbose))) { + MOZ_LOG(gPageCacheLog, LogLevel::Verbose, + ("Previous index: %d, Loaded index: %d", mPreviousEntryIndex, + mLoadedEntryIndex)); + } + } + + return rv; +} + +NS_IMETHODIMP +nsDocShell::GetCurrentSHEntry(nsISHEntry** aEntry, bool* aOSHE) { + *aOSHE = false; + *aEntry = nullptr; + if (mLSHE) { + NS_ADDREF(*aEntry = mLSHE); + } else if (mOSHE) { + NS_ADDREF(*aEntry = mOSHE); + *aOSHE = true; + } + return NS_OK; +} + +NS_IMETHODIMP nsDocShell::SynchronizeLayoutHistoryState() { + if (mActiveEntry && mActiveEntry->GetLayoutHistoryState() && + mBrowsingContext) { + if (XRE_IsContentProcess()) { + dom::ContentChild* contentChild = dom::ContentChild::GetSingleton(); + if (contentChild) { + contentChild->SendSynchronizeLayoutHistoryState( + mBrowsingContext, mActiveEntry->GetLayoutHistoryState()); + } + } else { + SessionHistoryEntry* entry = + mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry(); + if (entry) { + entry->SetLayoutHistoryState(mActiveEntry->GetLayoutHistoryState()); + } + } + if (mLoadingEntry && + mLoadingEntry->mInfo.SharedId() == mActiveEntry->SharedId()) { + mLoadingEntry->mInfo.SetLayoutHistoryState( + mActiveEntry->GetLayoutHistoryState()); + } + } + + return NS_OK; +} + +void nsDocShell::SetLoadGroupDefaultLoadFlags(nsLoadFlags aLoadFlags) { + if (mLoadGroup) { + mLoadGroup->SetDefaultLoadFlags(aLoadFlags); + } else { + NS_WARNING( + "nsDocShell::SetLoadGroupDefaultLoadFlags has no loadGroup to " + "propagate the mode to"); + } +} + +nsIScriptGlobalObject* nsDocShell::GetScriptGlobalObject() { + NS_ENSURE_SUCCESS(EnsureScriptEnvironment(), nullptr); + return mScriptGlobal; +} + +Document* nsDocShell::GetDocument() { + NS_ENSURE_SUCCESS(EnsureContentViewer(), nullptr); + return mContentViewer->GetDocument(); +} + +Document* nsDocShell::GetExtantDocument() { + return mContentViewer ? mContentViewer->GetDocument() : nullptr; +} + +nsPIDOMWindowOuter* nsDocShell::GetWindow() { + if (NS_FAILED(EnsureScriptEnvironment())) { + return nullptr; + } + return mScriptGlobal; +} + +NS_IMETHODIMP +nsDocShell::GetDomWindow(mozIDOMWindowProxy** aWindow) { + NS_ENSURE_ARG_POINTER(aWindow); + + nsresult rv = EnsureScriptEnvironment(); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsGlobalWindowOuter> window = mScriptGlobal; + window.forget(aWindow); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) { + RefPtr<ContentFrameMessageManager> mm; + if (RefPtr<BrowserChild> browserChild = BrowserChild::GetFrom(this)) { + mm = browserChild->GetMessageManager(); + } else if (nsPIDOMWindowOuter* win = GetWindow()) { + mm = win->GetMessageManager(); + } + mm.forget(aMessageManager); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetIsNavigating(bool* aOut) { + *aOut = mIsNavigating; + return NS_OK; +} + +void nsDocShell::ClearFrameHistory(nsISHEntry* aEntry) { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (!rootSH || !aEntry) { + return; + } + + rootSH->LegacySHistory()->RemoveFrameEntries(aEntry); +} + +//------------------------------------- +//-- Helper Method for Print discovery +//------------------------------------- +bool nsDocShell::NavigationBlockedByPrinting(bool aDisplayErrorDialog) { + if (!mBrowsingContext->Top()->GetIsPrinting()) { + return false; + } + if (aDisplayErrorDialog) { + DisplayLoadError(NS_ERROR_DOCUMENT_IS_PRINTMODE, nullptr, nullptr, nullptr); + } + return true; +} + +bool nsDocShell::IsNavigationAllowed(bool aDisplayPrintErrorDialog, + bool aCheckIfUnloadFired) { + bool isAllowed = !NavigationBlockedByPrinting(aDisplayPrintErrorDialog) && + (!aCheckIfUnloadFired || !mFiredUnloadEvent); + if (!isAllowed) { + return false; + } + if (!mContentViewer) { + return true; + } + bool firingBeforeUnload; + mContentViewer->GetBeforeUnloadFiring(&firingBeforeUnload); + return !firingBeforeUnload; +} + +//***************************************************************************** +// nsDocShell::nsIWebNavigation +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::GetCanGoBack(bool* aCanGoBack) { + *aCanGoBack = false; + if (!IsNavigationAllowed(false)) { + return NS_OK; // JS may not handle returning of an error code + } + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (rootSH) { + *aCanGoBack = rootSH->CanGo(-1); + MOZ_LOG(gSHLog, LogLevel::Verbose, + ("nsDocShell %p CanGoBack()->%d", this, *aCanGoBack)); + + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsDocShell::GetCanGoForward(bool* aCanGoForward) { + *aCanGoForward = false; + if (!IsNavigationAllowed(false)) { + return NS_OK; // JS may not handle returning of an error code + } + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (rootSH) { + *aCanGoForward = rootSH->CanGo(1); + MOZ_LOG(gSHLog, LogLevel::Verbose, + ("nsDocShell %p CanGoForward()->%d", this, *aCanGoForward)); + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsDocShell::GoBack(bool aRequireUserInteraction, bool aUserActivation) { + if (!IsNavigationAllowed()) { + return NS_OK; // JS may not handle returning of an error code + } + + auto cleanupIsNavigating = MakeScopeExit([&]() { mIsNavigating = false; }); + mIsNavigating = true; + + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + NS_ENSURE_TRUE(rootSH, NS_ERROR_FAILURE); + ErrorResult rv; + rootSH->Go(-1, aRequireUserInteraction, aUserActivation, rv); + return rv.StealNSResult(); +} + +NS_IMETHODIMP +nsDocShell::GoForward(bool aRequireUserInteraction, bool aUserActivation) { + if (!IsNavigationAllowed()) { + return NS_OK; // JS may not handle returning of an error code + } + + auto cleanupIsNavigating = MakeScopeExit([&]() { mIsNavigating = false; }); + mIsNavigating = true; + + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + NS_ENSURE_TRUE(rootSH, NS_ERROR_FAILURE); + ErrorResult rv; + rootSH->Go(1, aRequireUserInteraction, aUserActivation, rv); + return rv.StealNSResult(); +} + +// XXX(nika): We may want to stop exposing this API in the child process? Going +// to a specific index from multiple different processes could definitely race. +NS_IMETHODIMP +nsDocShell::GotoIndex(int32_t aIndex, bool aUserActivation) { + if (!IsNavigationAllowed()) { + return NS_OK; // JS may not handle returning of an error code + } + + auto cleanupIsNavigating = MakeScopeExit([&]() { mIsNavigating = false; }); + mIsNavigating = true; + + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + NS_ENSURE_TRUE(rootSH, NS_ERROR_FAILURE); + + ErrorResult rv; + rootSH->GotoIndex(aIndex, aIndex - rootSH->Index(), false, aUserActivation, + rv); + return rv.StealNSResult(); +} + +nsresult nsDocShell::LoadURI(nsIURI* aURI, + const LoadURIOptions& aLoadURIOptions) { + if (!IsNavigationAllowed()) { + return NS_OK; // JS may not handle returning of an error code + } + RefPtr<nsDocShellLoadState> loadState; + nsresult rv = nsDocShellLoadState::CreateFromLoadURIOptions( + mBrowsingContext, aURI, aLoadURIOptions, getter_AddRefs(loadState)); + MOZ_ASSERT(rv != NS_ERROR_MALFORMED_URI); + if (NS_FAILED(rv) || !loadState) { + return NS_ERROR_FAILURE; + } + + return LoadURI(loadState, true); +} + +NS_IMETHODIMP +nsDocShell::LoadURIFromScript(nsIURI* aURI, + JS::Handle<JS::Value> aLoadURIOptions, + JSContext* aCx) { + // generate dictionary for aLoadURIOptions and forward call + LoadURIOptions loadURIOptions; + if (!loadURIOptions.Init(aCx, aLoadURIOptions)) { + return NS_ERROR_INVALID_ARG; + } + return LoadURI(aURI, loadURIOptions); +} + +nsresult nsDocShell::FixupAndLoadURIString( + const nsAString& aURIString, const LoadURIOptions& aLoadURIOptions) { + if (!IsNavigationAllowed()) { + return NS_OK; // JS may not handle returning of an error code + } + + RefPtr<nsDocShellLoadState> loadState; + nsresult rv = nsDocShellLoadState::CreateFromLoadURIOptions( + mBrowsingContext, aURIString, aLoadURIOptions, getter_AddRefs(loadState)); + + uint32_t loadFlags = aLoadURIOptions.mLoadFlags; + if (NS_ERROR_MALFORMED_URI == rv) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("Creating an active entry on nsDocShell %p to %s (because " + "we're showing an error page)", + this, NS_ConvertUTF16toUTF8(aURIString).get())); + + // We need to store a session history entry. We don't have a valid URI, so + // we use about:blank instead. + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), "about:blank"_ns)); + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + if (aLoadURIOptions.mTriggeringPrincipal) { + triggeringPrincipal = aLoadURIOptions.mTriggeringPrincipal; + } else { + triggeringPrincipal = nsContentUtils::GetSystemPrincipal(); + } + if (mozilla::SessionHistoryInParent()) { + mActiveEntry = MakeUnique<SessionHistoryInfo>( + uri, triggeringPrincipal, nullptr, nullptr, nullptr, + nsLiteralCString("text/html")); + mBrowsingContext->SetActiveSessionHistoryEntry( + Nothing(), mActiveEntry.get(), MAKE_LOAD_TYPE(LOAD_NORMAL, loadFlags), + /* aUpdatedCacheKey = */ 0); + } + if (DisplayLoadError(rv, nullptr, PromiseFlatString(aURIString).get(), + nullptr) && + (loadFlags & LOAD_FLAGS_ERROR_LOAD_CHANGES_RV) != 0) { + return NS_ERROR_LOAD_SHOWED_ERRORPAGE; + } + } + + if (NS_FAILED(rv) || !loadState) { + return NS_ERROR_FAILURE; + } + + return LoadURI(loadState, true); +} + +NS_IMETHODIMP +nsDocShell::FixupAndLoadURIStringFromScript( + const nsAString& aURIString, JS::Handle<JS::Value> aLoadURIOptions, + JSContext* aCx) { + // generate dictionary for aLoadURIOptions and forward call + LoadURIOptions loadURIOptions; + if (!loadURIOptions.Init(aCx, aLoadURIOptions)) { + return NS_ERROR_INVALID_ARG; + } + return FixupAndLoadURIString(aURIString, loadURIOptions); +} + +void nsDocShell::UnblockEmbedderLoadEventForFailure(bool aFireFrameErrorEvent) { + // If we're not in a content frame, or are at a BrowsingContext tree boundary, + // such as the content-chrome boundary, don't fire the error event. + if (mBrowsingContext->IsTopContent() || mBrowsingContext->IsChrome()) { + return; + } + + // If embedder is same-process, then unblocking the load event is already + // handled by nsDocLoader. Fire the error event on our embedder element if + // requested. + // + // XXX: Bug 1440212 is looking into potentially changing this behaviour to act + // more like the remote case when in-process. + RefPtr<Element> element = mBrowsingContext->GetEmbedderElement(); + if (element) { + if (aFireFrameErrorEvent) { + if (RefPtr<nsFrameLoaderOwner> flo = do_QueryObject(element)) { + if (RefPtr<nsFrameLoader> fl = flo->GetFrameLoader()) { + fl->FireErrorEvent(); + } + } + } + return; + } + + // If we have a cross-process parent document, we must notify it that we no + // longer block its load event. This is necessary for OOP sub-documents + // because error documents do not result in a call to + // SendMaybeFireEmbedderLoadEvents via any of the normal call paths. + // (Obviously, we must do this before any of the returns below.) + RefPtr<BrowserChild> browserChild = BrowserChild::GetFrom(this); + if (browserChild) { + mozilla::Unused << browserChild->SendMaybeFireEmbedderLoadEvents( + aFireFrameErrorEvent ? EmbedderElementEventType::ErrorEvent + : EmbedderElementEventType::NoEvent); + } +} + +NS_IMETHODIMP +nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI, + const char16_t* aURL, nsIChannel* aFailedChannel, + bool* aDisplayedErrorPage) { + MOZ_LOG(gDocShellLeakLog, LogLevel::Debug, + ("DOCSHELL %p DisplayLoadError %s\n", this, + aURI ? aURI->GetSpecOrDefault().get() : "")); + + *aDisplayedErrorPage = false; + // Get prompt and string bundle services + nsCOMPtr<nsIPrompt> prompter; + nsCOMPtr<nsIStringBundle> stringBundle; + GetPromptAndStringBundle(getter_AddRefs(prompter), + getter_AddRefs(stringBundle)); + + NS_ENSURE_TRUE(stringBundle, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(prompter, NS_ERROR_FAILURE); + + const char* error = nullptr; + // The key used to select the appropriate error message from the properties + // file. + const char* errorDescriptionID = nullptr; + AutoTArray<nsString, 3> formatStrs; + bool addHostPort = false; + bool isBadStsCertError = false; + nsresult rv = NS_OK; + nsAutoString messageStr; + nsAutoCString cssClass; + nsAutoCString errorPage; + + errorPage.AssignLiteral("neterror"); + + // Turn the error code into a human readable error message. + if (NS_ERROR_UNKNOWN_PROTOCOL == aError) { + NS_ENSURE_ARG_POINTER(aURI); + + // Extract the schemes into a comma delimited list. + nsAutoCString scheme; + aURI->GetScheme(scheme); + CopyASCIItoUTF16(scheme, *formatStrs.AppendElement()); + nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(aURI); + while (nestedURI) { + nsCOMPtr<nsIURI> tempURI; + nsresult rv2; + rv2 = nestedURI->GetInnerURI(getter_AddRefs(tempURI)); + if (NS_SUCCEEDED(rv2) && tempURI) { + tempURI->GetScheme(scheme); + formatStrs[0].AppendLiteral(", "); + AppendASCIItoUTF16(scheme, formatStrs[0]); + } + nestedURI = do_QueryInterface(tempURI); + } + error = "unknownProtocolFound"; + } else if (NS_ERROR_FILE_NOT_FOUND == aError) { + NS_ENSURE_ARG_POINTER(aURI); + error = "fileNotFound"; + } else if (NS_ERROR_FILE_ACCESS_DENIED == aError) { + NS_ENSURE_ARG_POINTER(aURI); + error = "fileAccessDenied"; + } else if (NS_ERROR_UNKNOWN_HOST == aError) { + NS_ENSURE_ARG_POINTER(aURI); + // Get the host + nsAutoCString host; + nsCOMPtr<nsIURI> innermostURI = NS_GetInnermostURI(aURI); + innermostURI->GetHost(host); + CopyUTF8toUTF16(host, *formatStrs.AppendElement()); + errorDescriptionID = "dnsNotFound2"; + error = "dnsNotFound"; + } else if (NS_ERROR_CONNECTION_REFUSED == aError || + NS_ERROR_PROXY_BAD_GATEWAY == aError) { + NS_ENSURE_ARG_POINTER(aURI); + addHostPort = true; + error = "connectionFailure"; + } else if (NS_ERROR_NET_INTERRUPT == aError) { + NS_ENSURE_ARG_POINTER(aURI); + addHostPort = true; + error = "netInterrupt"; + } else if (NS_ERROR_NET_TIMEOUT == aError || + NS_ERROR_PROXY_GATEWAY_TIMEOUT == aError || + NS_ERROR_NET_TIMEOUT_EXTERNAL == aError) { + NS_ENSURE_ARG_POINTER(aURI); + // Get the host + nsAutoCString host; + aURI->GetHost(host); + CopyUTF8toUTF16(host, *formatStrs.AppendElement()); + error = "netTimeout"; + } else if (NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION == aError || + NS_ERROR_CSP_FORM_ACTION_VIOLATION == aError || + NS_ERROR_CSP_NAVIGATE_TO_VIOLATION == aError) { + // CSP error + cssClass.AssignLiteral("neterror"); + error = "cspBlocked"; + } else if (NS_ERROR_XFO_VIOLATION == aError) { + // XFO error + cssClass.AssignLiteral("neterror"); + error = "xfoBlocked"; + } else if (NS_ERROR_GET_MODULE(aError) == NS_ERROR_MODULE_SECURITY) { + nsCOMPtr<nsINSSErrorsService> nsserr = + do_GetService(NS_NSS_ERRORS_SERVICE_CONTRACTID); + + uint32_t errorClass; + if (!nsserr || NS_FAILED(nsserr->GetErrorClass(aError, &errorClass))) { + errorClass = nsINSSErrorsService::ERROR_CLASS_SSL_PROTOCOL; + } + + nsCOMPtr<nsITransportSecurityInfo> tsi; + if (aFailedChannel) { + aFailedChannel->GetSecurityInfo(getter_AddRefs(tsi)); + } + if (tsi) { + uint32_t securityState; + tsi->GetSecurityState(&securityState); + if (securityState & nsIWebProgressListener::STATE_USES_SSL_3) { + error = "sslv3Used"; + addHostPort = true; + } else if (securityState & + nsIWebProgressListener::STATE_USES_WEAK_CRYPTO) { + error = "weakCryptoUsed"; + addHostPort = true; + } + } else { + // No channel, let's obtain the generic error message + if (nsserr) { + nsserr->GetErrorMessage(aError, messageStr); + } + } + // We don't have a message string here anymore but DisplayLoadError + // requires a non-empty messageStr. + messageStr.Truncate(); + messageStr.AssignLiteral(u" "); + if (errorClass == nsINSSErrorsService::ERROR_CLASS_BAD_CERT) { + error = "nssBadCert"; + + // If this is an HTTP Strict Transport Security host or a pinned host + // and the certificate is bad, don't allow overrides (RFC 6797 section + // 12.1). + bool isStsHost = false; + bool isPinnedHost = false; + OriginAttributes attrsForHSTS; + if (aFailedChannel) { + StoragePrincipalHelper::GetOriginAttributesForHSTS(aFailedChannel, + attrsForHSTS); + } else { + attrsForHSTS = GetOriginAttributes(); + } + + if (XRE_IsParentProcess()) { + nsCOMPtr<nsISiteSecurityService> sss = + do_GetService(NS_SSSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = sss->IsSecureURI(aURI, attrsForHSTS, &isStsHost); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mozilla::dom::ContentChild* cc = + mozilla::dom::ContentChild::GetSingleton(); + cc->SendIsSecureURI(aURI, attrsForHSTS, &isStsHost); + } + nsCOMPtr<nsIPublicKeyPinningService> pkps = + do_GetService(NS_PKPSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = pkps->HostHasPins(aURI, &isPinnedHost); + + if (Preferences::GetBool("browser.xul.error_pages.expert_bad_cert", + false)) { + cssClass.AssignLiteral("expertBadCert"); + } + + // HSTS/pinning takes precedence over the expert bad cert pref. We + // never want to show the "Add Exception" button for these sites. + // In the future we should differentiate between an HSTS host and a + // pinned host and display a more informative message to the user. + if (isStsHost || isPinnedHost) { + isBadStsCertError = true; + cssClass.AssignLiteral("badStsCert"); + } + + // See if an alternate cert error page is registered + nsAutoCString alternateErrorPage; + nsresult rv = Preferences::GetCString( + "security.alternate_certificate_error_page", alternateErrorPage); + if (NS_SUCCEEDED(rv)) { + errorPage.Assign(alternateErrorPage); + } + } else { + error = "nssFailure2"; + } + } else if (NS_ERROR_PHISHING_URI == aError || + NS_ERROR_MALWARE_URI == aError || + NS_ERROR_UNWANTED_URI == aError || + NS_ERROR_HARMFUL_URI == aError) { + nsAutoCString host; + aURI->GetHost(host); + CopyUTF8toUTF16(host, *formatStrs.AppendElement()); + + // Malware and phishing detectors may want to use an alternate error + // page, but if the pref's not set, we'll fall back on the standard page + nsAutoCString alternateErrorPage; + nsresult rv = Preferences::GetCString("urlclassifier.alternate_error_page", + alternateErrorPage); + if (NS_SUCCEEDED(rv)) { + errorPage.Assign(alternateErrorPage); + } + + if (NS_ERROR_PHISHING_URI == aError) { + error = "deceptiveBlocked"; + } else if (NS_ERROR_MALWARE_URI == aError) { + error = "malwareBlocked"; + } else if (NS_ERROR_UNWANTED_URI == aError) { + error = "unwantedBlocked"; + } else if (NS_ERROR_HARMFUL_URI == aError) { + error = "harmfulBlocked"; + } + + cssClass.AssignLiteral("blacklist"); + } else if (NS_ERROR_CONTENT_CRASHED == aError) { + errorPage.AssignLiteral("tabcrashed"); + error = "tabcrashed"; + + RefPtr<EventTarget> handler = mChromeEventHandler; + if (handler) { + nsCOMPtr<Element> element = do_QueryInterface(handler); + element->GetAttribute(u"crashedPageTitle"_ns, messageStr); + } + + // DisplayLoadError requires a non-empty messageStr to proceed and call + // LoadErrorPage. If the page doesn't have a title, we will use a blank + // space which will be trimmed and thus treated as empty by the front-end. + if (messageStr.IsEmpty()) { + messageStr.AssignLiteral(u" "); + } + } else if (NS_ERROR_FRAME_CRASHED == aError) { + errorPage.AssignLiteral("framecrashed"); + error = "framecrashed"; + messageStr.AssignLiteral(u" "); + } else if (NS_ERROR_BUILDID_MISMATCH == aError) { + errorPage.AssignLiteral("restartrequired"); + error = "restartrequired"; + + // DisplayLoadError requires a non-empty messageStr to proceed and call + // LoadErrorPage. If the page doesn't have a title, we will use a blank + // space which will be trimmed and thus treated as empty by the front-end. + if (messageStr.IsEmpty()) { + messageStr.AssignLiteral(u" "); + } + } else { + // Errors requiring simple formatting + switch (aError) { + case NS_ERROR_MALFORMED_URI: + // URI is malformed + error = "malformedURI"; + errorDescriptionID = "malformedURI2"; + break; + case NS_ERROR_REDIRECT_LOOP: + // Doc failed to load because the server generated too many redirects + error = "redirectLoop"; + break; + case NS_ERROR_UNKNOWN_SOCKET_TYPE: + // Doc failed to load because PSM is not installed + error = "unknownSocketType"; + break; + case NS_ERROR_NET_RESET: + // Doc failed to load because the server kept reseting the connection + // before we could read any data from it + error = "netReset"; + break; + case NS_ERROR_DOCUMENT_NOT_CACHED: + // Doc failed to load because the cache does not contain a copy of + // the document. + error = "notCached"; + break; + case NS_ERROR_OFFLINE: + // Doc failed to load because we are offline. + error = "netOffline"; + break; + case NS_ERROR_DOCUMENT_IS_PRINTMODE: + // Doc navigation attempted while Printing or Print Preview + error = "isprinting"; + break; + case NS_ERROR_PORT_ACCESS_NOT_ALLOWED: + // Port blocked for security reasons + addHostPort = true; + error = "deniedPortAccess"; + break; + case NS_ERROR_UNKNOWN_PROXY_HOST: + // Proxy hostname could not be resolved. + error = "proxyResolveFailure"; + break; + case NS_ERROR_PROXY_CONNECTION_REFUSED: + case NS_ERROR_PROXY_FORBIDDEN: + case NS_ERROR_PROXY_NOT_IMPLEMENTED: + case NS_ERROR_PROXY_AUTHENTICATION_FAILED: + case NS_ERROR_PROXY_TOO_MANY_REQUESTS: + // Proxy connection was refused. + error = "proxyConnectFailure"; + break; + case NS_ERROR_INVALID_CONTENT_ENCODING: + // Bad Content Encoding. + error = "contentEncodingError"; + break; + case NS_ERROR_UNSAFE_CONTENT_TYPE: + // Channel refused to load from an unrecognized content type. + error = "unsafeContentType"; + break; + case NS_ERROR_CORRUPTED_CONTENT: + // Broken Content Detected. e.g. Content-MD5 check failure. + error = "corruptedContentErrorv2"; + break; + case NS_ERROR_INTERCEPTION_FAILED: + // ServiceWorker intercepted request, but something went wrong. + error = "corruptedContentErrorv2"; + break; + case NS_ERROR_NET_INADEQUATE_SECURITY: + // Server negotiated bad TLS for HTTP/2. + error = "inadequateSecurityError"; + addHostPort = true; + break; + case NS_ERROR_BLOCKED_BY_POLICY: + case NS_ERROR_DOM_COOP_FAILED: + case NS_ERROR_DOM_COEP_FAILED: + // Page blocked by policy + error = "blockedByPolicy"; + break; + case NS_ERROR_NET_HTTP2_SENT_GOAWAY: + case NS_ERROR_NET_HTTP3_PROTOCOL_ERROR: + // HTTP/2 or HTTP/3 stack detected a protocol error + error = "networkProtocolError"; + break; + + default: + break; + } + } + + nsresult delegateErrorCode = aError; + // If the HTTPS-Only Mode upgraded this request and the upgrade might have + // caused this error, we replace the error-page with about:httpsonlyerror + if (nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(aFailedChannel, aError)) { + errorPage.AssignLiteral("httpsonlyerror"); + delegateErrorCode = NS_ERROR_HTTPS_ONLY; + } else if (isBadStsCertError) { + delegateErrorCode = NS_ERROR_BAD_HSTS_CERT; + } + + if (nsCOMPtr<nsILoadURIDelegate> loadURIDelegate = GetLoadURIDelegate()) { + nsCOMPtr<nsIURI> errorPageURI; + rv = loadURIDelegate->HandleLoadError( + aURI, delegateErrorCode, NS_ERROR_GET_MODULE(delegateErrorCode), + getter_AddRefs(errorPageURI)); + // If the docshell is going away there's no point in showing an error page. + if (NS_FAILED(rv) || mIsBeingDestroyed) { + *aDisplayedErrorPage = false; + return NS_OK; + } + + if (errorPageURI) { + *aDisplayedErrorPage = + NS_SUCCEEDED(LoadErrorPage(errorPageURI, aURI, aFailedChannel)); + return NS_OK; + } + } + + // Test if the error should be displayed + if (!error) { + return NS_OK; + } + + if (!errorDescriptionID) { + errorDescriptionID = error; + } + + Telemetry::AccumulateCategoricalKeyed( + IsSubframe() ? "frame"_ns : "top"_ns, + mozilla::dom::LoadErrorToTelemetryLabel(aError)); + + // Test if the error needs to be formatted + if (!messageStr.IsEmpty()) { + // already obtained message + } else { + if (addHostPort) { + // Build up the host:port string. + nsAutoCString hostport; + if (aURI) { + aURI->GetHostPort(hostport); + } else { + hostport.Assign('?'); + } + CopyUTF8toUTF16(hostport, *formatStrs.AppendElement()); + } + + nsAutoCString spec; + rv = NS_ERROR_NOT_AVAILABLE; + auto& nextFormatStr = *formatStrs.AppendElement(); + if (aURI) { + // displaying "file://" is aesthetically unpleasing and could even be + // confusing to the user + if (SchemeIsFile(aURI)) { + aURI->GetPathQueryRef(spec); + } else { + aURI->GetSpec(spec); + } + + nsCOMPtr<nsITextToSubURI> textToSubURI( + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv)); + if (NS_SUCCEEDED(rv)) { + rv = textToSubURI->UnEscapeURIForUI(spec, nextFormatStr); + } + } else { + spec.Assign('?'); + } + if (NS_FAILED(rv)) { + CopyUTF8toUTF16(spec, nextFormatStr); + } + rv = NS_OK; + + nsAutoString str; + rv = + stringBundle->FormatStringFromName(errorDescriptionID, formatStrs, str); + NS_ENSURE_SUCCESS(rv, rv); + messageStr.Assign(str); + } + + // Display the error as a page or an alert prompt + NS_ENSURE_FALSE(messageStr.IsEmpty(), NS_ERROR_FAILURE); + + if ((NS_ERROR_NET_INTERRUPT == aError || NS_ERROR_NET_RESET == aError) && + SchemeIsHTTPS(aURI)) { + // Maybe TLS intolerant. Treat this as an SSL error. + error = "nssFailure2"; + } + + if (mBrowsingContext->GetUseErrorPages()) { + // Display an error page + nsresult loadedPage = + LoadErrorPage(aURI, aURL, errorPage.get(), error, messageStr.get(), + cssClass.get(), aFailedChannel); + *aDisplayedErrorPage = NS_SUCCEEDED(loadedPage); + } else { + // The prompter reqires that our private window has a document (or it + // asserts). Satisfy that assertion now since GetDoc will force + // creation of one if it hasn't already been created. + if (mScriptGlobal) { + Unused << mScriptGlobal->GetDoc(); + } + + // Display a message box + prompter->Alert(nullptr, messageStr.get()); + } + + return NS_OK; +} + +#define PREF_SAFEBROWSING_ALLOWOVERRIDE "browser.safebrowsing.allowOverride" + +nsresult nsDocShell::LoadErrorPage(nsIURI* aURI, const char16_t* aURL, + const char* aErrorPage, + const char* aErrorType, + const char16_t* aDescription, + const char* aCSSClass, + nsIChannel* aFailedChannel) { + if (mIsBeingDestroyed) { + return NS_ERROR_NOT_AVAILABLE; + } + +#if defined(DEBUG) + if (MOZ_LOG_TEST(gDocShellLog, LogLevel::Debug)) { + nsAutoCString chanName; + if (aFailedChannel) { + aFailedChannel->GetName(chanName); + } else { + chanName.AssignLiteral("<no channel>"); + } + + MOZ_LOG(gDocShellLog, LogLevel::Debug, + ("nsDocShell[%p]::LoadErrorPage(\"%s\", \"%s\", {...}, [%s])\n", + this, aURI ? aURI->GetSpecOrDefault().get() : "", + NS_ConvertUTF16toUTF8(aURL).get(), chanName.get())); + } +#endif + + nsAutoCString url; + if (aURI) { + nsresult rv = aURI->GetSpec(url); + NS_ENSURE_SUCCESS(rv, rv); + } else if (aURL) { + CopyUTF16toUTF8(MakeStringSpan(aURL), url); + } else { + return NS_ERROR_INVALID_POINTER; + } + + // Create a URL to pass all the error information through to the page. + +#undef SAFE_ESCAPE +#define SAFE_ESCAPE(output, input, params) \ + if (NS_WARN_IF(!NS_Escape(input, output, params))) { \ + return NS_ERROR_OUT_OF_MEMORY; \ + } + + nsCString escapedUrl, escapedError, escapedDescription, escapedCSSClass; + SAFE_ESCAPE(escapedUrl, url, url_Path); + SAFE_ESCAPE(escapedError, nsDependentCString(aErrorType), url_Path); + SAFE_ESCAPE(escapedDescription, NS_ConvertUTF16toUTF8(aDescription), + url_Path); + if (aCSSClass) { + nsCString cssClass(aCSSClass); + SAFE_ESCAPE(escapedCSSClass, cssClass, url_Path); + } + nsCString errorPageUrl("about:"); + errorPageUrl.AppendASCII(aErrorPage); + errorPageUrl.AppendLiteral("?e="); + + errorPageUrl.AppendASCII(escapedError.get()); + errorPageUrl.AppendLiteral("&u="); + errorPageUrl.AppendASCII(escapedUrl.get()); + if ((strcmp(aErrorPage, "blocked") == 0) && + Preferences::GetBool(PREF_SAFEBROWSING_ALLOWOVERRIDE, true)) { + errorPageUrl.AppendLiteral("&o=1"); + } + if (!escapedCSSClass.IsEmpty()) { + errorPageUrl.AppendLiteral("&s="); + errorPageUrl.AppendASCII(escapedCSSClass.get()); + } + errorPageUrl.AppendLiteral("&c=UTF-8"); + + nsCOMPtr<nsICaptivePortalService> cps = do_GetService(NS_CAPTIVEPORTAL_CID); + int32_t cpsState; + if (cps && NS_SUCCEEDED(cps->GetState(&cpsState)) && + cpsState == nsICaptivePortalService::LOCKED_PORTAL) { + errorPageUrl.AppendLiteral("&captive=true"); + } + + errorPageUrl.AppendLiteral("&d="); + errorPageUrl.AppendASCII(escapedDescription.get()); + + nsCOMPtr<nsIURI> errorPageURI; + nsresult rv = NS_NewURI(getter_AddRefs(errorPageURI), errorPageUrl); + NS_ENSURE_SUCCESS(rv, rv); + + return LoadErrorPage(errorPageURI, aURI, aFailedChannel); +} + +nsresult nsDocShell::LoadErrorPage(nsIURI* aErrorURI, nsIURI* aFailedURI, + nsIChannel* aFailedChannel) { + mFailedChannel = aFailedChannel; + mFailedURI = aFailedURI; + mFailedLoadType = mLoadType; + + if (mLSHE) { + // Abandon mLSHE's BFCache entry and create a new one. This way, if + // we go back or forward to another SHEntry with the same doc + // identifier, the error page won't persist. + mLSHE->AbandonBFCacheEntry(); + } + + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aErrorURI); + loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal()); + if (mBrowsingContext) { + loadState->SetTriggeringSandboxFlags(mBrowsingContext->GetSandboxFlags()); + } + loadState->SetLoadType(LOAD_ERROR_PAGE); + loadState->SetFirstParty(true); + loadState->SetSourceBrowsingContext(mBrowsingContext); + if (mozilla::SessionHistoryInParent() && mLoadingEntry) { + // We keep the loading entry for the load that failed here. If the user + // reloads we want to try to reload the original load, not the error page. + loadState->SetLoadingSessionHistoryInfo( + MakeUnique<LoadingSessionHistoryInfo>(*mLoadingEntry)); + } + return InternalLoad(loadState); +} + +NS_IMETHODIMP +nsDocShell::Reload(uint32_t aReloadFlags) { + if (!IsNavigationAllowed()) { + return NS_OK; // JS may not handle returning of an error code + } + + NS_ASSERTION(((aReloadFlags & INTERNAL_LOAD_FLAGS_LOADURI_SETUP_FLAGS) == 0), + "Reload command not updated to use load flags!"); + NS_ASSERTION((aReloadFlags & EXTRA_LOAD_FLAGS) == 0, + "Don't pass these flags to Reload"); + + uint32_t loadType = MAKE_LOAD_TYPE(LOAD_RELOAD_NORMAL, aReloadFlags); + NS_ENSURE_TRUE(IsValidLoadType(loadType), NS_ERROR_INVALID_ARG); + + // Send notifications to the HistoryListener if any, about the impending + // reload + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (mozilla::SessionHistoryInParent()) { + MOZ_LOG(gSHLog, LogLevel::Debug, ("nsDocShell %p Reload", this)); + bool forceReload = IsForceReloadType(loadType); + if (!XRE_IsParentProcess()) { + ++mPendingReloadCount; + RefPtr<nsDocShell> docShell(this); + nsCOMPtr<nsIContentViewer> cv(mContentViewer); + NS_ENSURE_STATE(cv); + + bool okToUnload = true; + MOZ_TRY(cv->PermitUnload(&okToUnload)); + if (!okToUnload) { + return NS_OK; + } + + RefPtr<Document> doc(GetDocument()); + RefPtr<BrowsingContext> browsingContext(mBrowsingContext); + nsCOMPtr<nsIURI> currentURI(mCurrentURI); + nsCOMPtr<nsIReferrerInfo> referrerInfo(mReferrerInfo); + RefPtr<StopDetector> stopDetector = new StopDetector(); + nsCOMPtr<nsILoadGroup> loadGroup; + GetLoadGroup(getter_AddRefs(loadGroup)); + if (loadGroup) { + // loadGroup may be null in theory. In that case stopDetector just + // doesn't do anything. + loadGroup->AddRequest(stopDetector, nullptr); + } + + ContentChild::GetSingleton()->SendNotifyOnHistoryReload( + mBrowsingContext, forceReload, + [docShell, doc, loadType, browsingContext, currentURI, referrerInfo, + loadGroup, stopDetector]( + std::tuple<bool, Maybe<NotNull<RefPtr<nsDocShellLoadState>>>, + Maybe<bool>>&& aResult) { + auto scopeExit = MakeScopeExit([loadGroup, stopDetector]() { + if (loadGroup) { + loadGroup->RemoveRequest(stopDetector, nullptr, NS_OK); + } + }); + + // Decrease mPendingReloadCount before any other early returns! + if (--(docShell->mPendingReloadCount) > 0) { + return; + } + + if (stopDetector->Canceled()) { + return; + } + bool canReload; + Maybe<NotNull<RefPtr<nsDocShellLoadState>>> loadState; + Maybe<bool> reloadingActiveEntry; + + std::tie(canReload, loadState, reloadingActiveEntry) = aResult; + + if (!canReload) { + return; + } + + if (loadState.isSome()) { + MOZ_LOG( + gSHLog, LogLevel::Debug, + ("nsDocShell %p Reload - LoadHistoryEntry", docShell.get())); + loadState.ref()->SetNotifiedBeforeUnloadListeners(true); + docShell->LoadHistoryEntry(loadState.ref(), loadType, + reloadingActiveEntry.ref()); + } else { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p ReloadDocument", docShell.get())); + ReloadDocument(docShell, doc, loadType, browsingContext, + currentURI, referrerInfo, + /* aNotifiedBeforeUnloadListeners */ true); + } + }, + [](mozilla::ipc::ResponseRejectReason) {}); + } else { + // Parent process + bool canReload = false; + Maybe<NotNull<RefPtr<nsDocShellLoadState>>> loadState; + Maybe<bool> reloadingActiveEntry; + if (!mBrowsingContext->IsDiscarded()) { + mBrowsingContext->Canonical()->NotifyOnHistoryReload( + forceReload, canReload, loadState, reloadingActiveEntry); + } + if (canReload) { + if (loadState.isSome()) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p Reload - LoadHistoryEntry", this)); + LoadHistoryEntry(loadState.ref(), loadType, + reloadingActiveEntry.ref()); + } else { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p ReloadDocument", this)); + RefPtr<Document> doc = GetDocument(); + RefPtr<BrowsingContext> bc = mBrowsingContext; + nsCOMPtr<nsIURI> currentURI = mCurrentURI; + nsCOMPtr<nsIReferrerInfo> referrerInfo = mReferrerInfo; + ReloadDocument(this, doc, loadType, bc, currentURI, referrerInfo); + } + } + } + return NS_OK; + } + + bool canReload = true; + if (rootSH) { + rootSH->LegacySHistory()->NotifyOnHistoryReload(&canReload); + } + + if (!canReload) { + return NS_OK; + } + + /* If you change this part of code, make sure bug 45297 does not re-occur */ + if (mOSHE) { + nsCOMPtr<nsISHEntry> oshe = mOSHE; + return LoadHistoryEntry( + oshe, loadType, + aReloadFlags & nsIWebNavigation::LOAD_FLAGS_USER_ACTIVATION); + } + + if (mLSHE) { // In case a reload happened before the current load is done + nsCOMPtr<nsISHEntry> lshe = mLSHE; + return LoadHistoryEntry( + lshe, loadType, + aReloadFlags & nsIWebNavigation::LOAD_FLAGS_USER_ACTIVATION); + } + + RefPtr<Document> doc = GetDocument(); + RefPtr<BrowsingContext> bc = mBrowsingContext; + nsCOMPtr<nsIURI> currentURI = mCurrentURI; + nsCOMPtr<nsIReferrerInfo> referrerInfo = mReferrerInfo; + return ReloadDocument(this, doc, loadType, bc, currentURI, referrerInfo); +} + +/* static */ +nsresult nsDocShell::ReloadDocument(nsDocShell* aDocShell, Document* aDocument, + uint32_t aLoadType, + BrowsingContext* aBrowsingContext, + nsIURI* aCurrentURI, + nsIReferrerInfo* aReferrerInfo, + bool aNotifiedBeforeUnloadListeners) { + if (!aDocument) { + return NS_OK; + } + + // Do not inherit owner from document + uint32_t flags = INTERNAL_LOAD_FLAGS_NONE; + nsAutoString srcdoc; + nsIURI* baseURI = nullptr; + nsCOMPtr<nsIURI> originalURI; + nsCOMPtr<nsIURI> resultPrincipalURI; + bool loadReplace = false; + + nsIPrincipal* triggeringPrincipal = aDocument->NodePrincipal(); + nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp(); + uint32_t triggeringSandboxFlags = aDocument->GetSandboxFlags(); + + nsAutoString contentTypeHint; + aDocument->GetContentType(contentTypeHint); + + if (aDocument->IsSrcdocDocument()) { + aDocument->GetSrcdocData(srcdoc); + flags |= INTERNAL_LOAD_FLAGS_IS_SRCDOC; + baseURI = aDocument->GetBaseURI(); + } else { + srcdoc = VoidString(); + } + nsCOMPtr<nsIChannel> chan = aDocument->GetChannel(); + if (chan) { + uint32_t loadFlags; + chan->GetLoadFlags(&loadFlags); + loadReplace = loadFlags & nsIChannel::LOAD_REPLACE; + nsCOMPtr<nsIHttpChannel> httpChan(do_QueryInterface(chan)); + if (httpChan) { + httpChan->GetOriginalURI(getter_AddRefs(originalURI)); + } + + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + loadInfo->GetResultPrincipalURI(getter_AddRefs(resultPrincipalURI)); + } + + if (!triggeringPrincipal) { + MOZ_ASSERT(false, "Reload needs a valid triggeringPrincipal"); + return NS_ERROR_FAILURE; + } + + // Stack variables to ensure changes to the member variables don't affect to + // the call. + nsCOMPtr<nsIURI> currentURI = aCurrentURI; + + // Reload always rewrites result principal URI. + Maybe<nsCOMPtr<nsIURI>> emplacedResultPrincipalURI; + emplacedResultPrincipalURI.emplace(std::move(resultPrincipalURI)); + + RefPtr<WindowContext> context = aBrowsingContext->GetCurrentWindowContext(); + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(currentURI); + loadState->SetReferrerInfo(aReferrerInfo); + loadState->SetOriginalURI(originalURI); + loadState->SetMaybeResultPrincipalURI(emplacedResultPrincipalURI); + loadState->SetLoadReplace(loadReplace); + loadState->SetTriggeringPrincipal(triggeringPrincipal); + loadState->SetTriggeringSandboxFlags(triggeringSandboxFlags); + loadState->SetPrincipalToInherit(triggeringPrincipal); + loadState->SetCsp(csp); + loadState->SetInternalLoadFlags(flags); + loadState->SetTypeHint(NS_ConvertUTF16toUTF8(contentTypeHint)); + loadState->SetLoadType(aLoadType); + loadState->SetFirstParty(true); + loadState->SetSrcdocData(srcdoc); + loadState->SetSourceBrowsingContext(aBrowsingContext); + loadState->SetBaseURI(baseURI); + loadState->SetHasValidUserGestureActivation( + context && context->HasValidTransientUserGestureActivation()); + loadState->SetNotifiedBeforeUnloadListeners(aNotifiedBeforeUnloadListeners); + return aDocShell->InternalLoad(loadState); +} + +NS_IMETHODIMP +nsDocShell::Stop(uint32_t aStopFlags) { + // Revoke any pending event related to content viewer restoration + mRestorePresentationEvent.Revoke(); + + if (mLoadType == LOAD_ERROR_PAGE) { + if (mLSHE) { + // Since error page loads never unset mLSHE, do so now + SetHistoryEntryAndUpdateBC(Some(nullptr), Some<nsISHEntry*>(mLSHE)); + } + mActiveEntryIsLoadingFromSessionHistory = false; + + mFailedChannel = nullptr; + mFailedURI = nullptr; + } + + if (nsIWebNavigation::STOP_CONTENT & aStopFlags) { + // Stop the document loading and animations + if (mContentViewer) { + nsCOMPtr<nsIContentViewer> cv = mContentViewer; + cv->Stop(); + } + } else if (nsIWebNavigation::STOP_NETWORK & aStopFlags) { + // Stop the document loading only + if (mContentViewer) { + RefPtr<Document> doc = mContentViewer->GetDocument(); + if (doc) { + doc->StopDocumentLoad(); + } + } + } + + if (nsIWebNavigation::STOP_NETWORK & aStopFlags) { + // Suspend any timers that were set for this loader. We'll clear + // them out for good in CreateContentViewer. + if (mRefreshURIList) { + SuspendRefreshURIs(); + mSavedRefreshURIList.swap(mRefreshURIList); + mRefreshURIList = nullptr; + } + + // XXXbz We could also pass |this| to nsIURILoader::Stop. That will + // just call Stop() on us as an nsIDocumentLoader... We need fewer + // redundant apis! + Stop(); + + // Clear out mChannelToDisconnectOnPageHide. This page won't go in the + // BFCache now, and the Stop above will have removed the DocumentChannel + // from the loadgroup. + mChannelToDisconnectOnPageHide = 0; + } + + for (auto* child : mChildList.ForwardRange()) { + nsCOMPtr<nsIWebNavigation> shellAsNav(do_QueryObject(child)); + if (shellAsNav) { + shellAsNav->Stop(aStopFlags); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetDocument(Document** aDocument) { + NS_ENSURE_ARG_POINTER(aDocument); + NS_ENSURE_SUCCESS(EnsureContentViewer(), NS_ERROR_FAILURE); + + RefPtr<Document> doc = mContentViewer->GetDocument(); + if (!doc) { + return NS_ERROR_NOT_AVAILABLE; + } + + doc.forget(aDocument); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetCurrentURI(nsIURI** aURI) { + NS_ENSURE_ARG_POINTER(aURI); + + nsCOMPtr<nsIURI> uri = mCurrentURI; + uri.forget(aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetSessionHistoryXPCOM(nsISupports** aSessionHistory) { + NS_ENSURE_ARG_POINTER(aSessionHistory); + RefPtr<ChildSHistory> shistory = GetSessionHistory(); + shistory.forget(aSessionHistory); + return NS_OK; +} + +//***************************************************************************** +// nsDocShell::nsIWebPageDescriptor +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::LoadPageAsViewSource(nsIDocShell* aOtherDocShell, + const nsAString& aURI) { + if (!aOtherDocShell) { + return NS_ERROR_INVALID_POINTER; + } + nsCOMPtr<nsIURI> newURI; + nsresult rv = NS_NewURI(getter_AddRefs(newURI), aURI); + if (NS_FAILED(rv)) { + return rv; + } + + RefPtr<nsDocShellLoadState> loadState; + uint32_t cacheKey; + auto* otherDocShell = nsDocShell::Cast(aOtherDocShell); + if (mozilla::SessionHistoryInParent()) { + loadState = new nsDocShellLoadState(newURI); + if (!otherDocShell->FillLoadStateFromCurrentEntry(*loadState)) { + return NS_ERROR_INVALID_POINTER; + } + cacheKey = otherDocShell->GetCacheKeyFromCurrentEntry().valueOr(0); + } else { + nsCOMPtr<nsISHEntry> entry; + bool isOriginalSHE; + otherDocShell->GetCurrentSHEntry(getter_AddRefs(entry), &isOriginalSHE); + if (!entry) { + return NS_ERROR_INVALID_POINTER; + } + rv = entry->CreateLoadInfo(getter_AddRefs(loadState)); + NS_ENSURE_SUCCESS(rv, rv); + entry->GetCacheKey(&cacheKey); + loadState->SetURI(newURI); + loadState->SetSHEntry(nullptr); + } + + // We're doing a load of the page, via an API that + // is only exposed to system code. The triggering principal for this load + // should be the system principal. + loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal()); + loadState->SetOriginalURI(nullptr); + loadState->SetResultPrincipalURI(nullptr); + + return InternalLoad(loadState, Some(cacheKey)); +} + +NS_IMETHODIMP +nsDocShell::GetCurrentDescriptor(nsISupports** aPageDescriptor) { + MOZ_ASSERT(aPageDescriptor, "Null out param?"); + + *aPageDescriptor = nullptr; + + nsISHEntry* src = mOSHE ? mOSHE : mLSHE; + if (src) { + nsCOMPtr<nsISHEntry> dest; + + nsresult rv = src->Clone(getter_AddRefs(dest)); + if (NS_FAILED(rv)) { + return rv; + } + + // null out inappropriate cloned attributes... + dest->SetParent(nullptr); + dest->SetIsSubFrame(false); + + return CallQueryInterface(dest, aPageDescriptor); + } + + return NS_ERROR_NOT_AVAILABLE; +} + +already_AddRefed<nsIInputStream> nsDocShell::GetPostDataFromCurrentEntry() + const { + nsCOMPtr<nsIInputStream> postData; + if (mozilla::SessionHistoryInParent()) { + if (mActiveEntry) { + postData = mActiveEntry->GetPostData(); + } else if (mLoadingEntry) { + postData = mLoadingEntry->mInfo.GetPostData(); + } + } else { + if (mOSHE) { + postData = mOSHE->GetPostData(); + } else if (mLSHE) { + postData = mLSHE->GetPostData(); + } + } + + return postData.forget(); +} + +Maybe<uint32_t> nsDocShell::GetCacheKeyFromCurrentEntry() const { + if (mozilla::SessionHistoryInParent()) { + if (mActiveEntry) { + return Some(mActiveEntry->GetCacheKey()); + } + + if (mLoadingEntry) { + return Some(mLoadingEntry->mInfo.GetCacheKey()); + } + } else { + if (mOSHE) { + return Some(mOSHE->GetCacheKey()); + } + + if (mLSHE) { + return Some(mLSHE->GetCacheKey()); + } + } + + return Nothing(); +} + +bool nsDocShell::FillLoadStateFromCurrentEntry( + nsDocShellLoadState& aLoadState) { + if (mLoadingEntry) { + mLoadingEntry->mInfo.FillLoadInfo(aLoadState); + return true; + } + if (mActiveEntry) { + mActiveEntry->FillLoadInfo(aLoadState); + return true; + } + return false; +} + +//***************************************************************************** +// nsDocShell::nsIBaseWindow +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::InitWindow(nativeWindow aParentNativeWindow, + nsIWidget* aParentWidget, int32_t aX, int32_t aY, + int32_t aWidth, int32_t aHeight) { + SetParentWidget(aParentWidget); + SetPositionAndSize(aX, aY, aWidth, aHeight, 0); + NS_ENSURE_TRUE(Initialize(), NS_ERROR_FAILURE); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::Destroy() { + // XXX: We allow this function to be called just once. If you are going to + // reset new variables in this function, please make sure the variables will + // never be re-initialized. Adding assertions to check |mIsBeingDestroyed| + // in the setter functions for the variables would be enough. + if (mIsBeingDestroyed) { + return NS_ERROR_DOCSHELL_DYING; + } + + NS_ASSERTION(mItemType == typeContent || mItemType == typeChrome, + "Unexpected item type in docshell"); + + nsCOMPtr<nsIObserverService> serv = services::GetObserverService(); + if (serv) { + const char* msg = mItemType == typeContent + ? NS_WEBNAVIGATION_DESTROY + : NS_CHROME_WEBNAVIGATION_DESTROY; + serv->NotifyObservers(GetAsSupports(this), msg, nullptr); + } + + mIsBeingDestroyed = true; + + // Brak the cycle with the initial client, if present. + mInitialClientSource.reset(); + + // Make sure we don't record profile timeline markers anymore + SetRecordProfileTimelineMarkers(false); + + // Make sure to blow away our mLoadingURI just in case. No loads + // from inside this pagehide. + mLoadingURI = nullptr; + + // Fire unload event before we blow anything away. + (void)FirePageHideNotification(true); + + // Clear pointers to any detached nsEditorData that's lying + // around in shistory entries. Breaks cycle. See bug 430921. + if (mOSHE) { + mOSHE->SetEditorData(nullptr); + } + if (mLSHE) { + mLSHE->SetEditorData(nullptr); + } + + // Note: mContentListener can be null if Init() failed and we're being + // called from the destructor. + if (mContentListener) { + mContentListener->DropDocShellReference(); + mContentListener->SetParentContentListener(nullptr); + // Note that we do NOT set mContentListener to null here; that + // way if someone tries to do a load in us after this point + // the nsDSURIContentListener will block it. All of which + // means that we should do this before calling Stop(), of + // course. + } + + // Stop any URLs that are currently being loaded... + Stop(nsIWebNavigation::STOP_ALL); + + mEditorData = nullptr; + + // Save the state of the current document, before destroying the window. + // This is needed to capture the state of a frameset when the new document + // causes the frameset to be destroyed... + PersistLayoutHistoryState(); + + // Remove this docshell from its parent's child list + nsCOMPtr<nsIDocShellTreeItem> docShellParentAsItem = + do_QueryInterface(GetAsSupports(mParent)); + if (docShellParentAsItem) { + docShellParentAsItem->RemoveChild(this); + } + + if (mContentViewer) { + mContentViewer->Close(nullptr); + mContentViewer->Destroy(); + mContentViewer = nullptr; + } + + nsDocLoader::Destroy(); + + mParentWidget = nullptr; + mCurrentURI = nullptr; + + if (mScriptGlobal) { + mScriptGlobal->DetachFromDocShell(!mWillChangeProcess); + mScriptGlobal = nullptr; + } + + if (GetSessionHistory()) { + // We want to destroy these content viewers now rather than + // letting their destruction wait for the session history + // entries to get garbage collected. (Bug 488394) + GetSessionHistory()->EvictLocalContentViewers(); + } + + if (mWillChangeProcess && !mBrowsingContext->IsDiscarded()) { + mBrowsingContext->PrepareForProcessChange(); + } + + SetTreeOwner(nullptr); + + mBrowserChild = nullptr; + + mChromeEventHandler = nullptr; + + // Cancel any timers that were set for this docshell; this is needed + // to break the cycle between us and the timers. + CancelRefreshURITimers(); + + return NS_OK; +} + +double nsDocShell::GetWidgetCSSToDeviceScale() { + if (mParentWidget) { + return mParentWidget->GetDefaultScale().scale; + } + if (nsCOMPtr<nsIBaseWindow> ownerWindow = do_QueryInterface(mTreeOwner)) { + return ownerWindow->GetWidgetCSSToDeviceScale(); + } + return 1.0; +} + +NS_IMETHODIMP +nsDocShell::GetDevicePixelsPerDesktopPixel(double* aScale) { + if (mParentWidget) { + *aScale = mParentWidget->GetDesktopToDeviceScale().scale; + return NS_OK; + } + + nsCOMPtr<nsIBaseWindow> ownerWindow(do_QueryInterface(mTreeOwner)); + if (ownerWindow) { + return ownerWindow->GetDevicePixelsPerDesktopPixel(aScale); + } + + *aScale = 1.0; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetPosition(int32_t aX, int32_t aY) { + mBounds.MoveTo(aX, aY); + + if (mContentViewer) { + NS_ENSURE_SUCCESS(mContentViewer->Move(aX, aY), NS_ERROR_FAILURE); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetPositionDesktopPix(int32_t aX, int32_t aY) { + nsCOMPtr<nsIBaseWindow> ownerWindow(do_QueryInterface(mTreeOwner)); + if (ownerWindow) { + return ownerWindow->SetPositionDesktopPix(aX, aY); + } + + double scale = 1.0; + GetDevicePixelsPerDesktopPixel(&scale); + return SetPosition(NSToIntRound(aX * scale), NSToIntRound(aY * scale)); +} + +NS_IMETHODIMP +nsDocShell::GetPosition(int32_t* aX, int32_t* aY) { + return GetPositionAndSize(aX, aY, nullptr, nullptr); +} + +NS_IMETHODIMP +nsDocShell::SetSize(int32_t aWidth, int32_t aHeight, bool aRepaint) { + int32_t x = 0, y = 0; + GetPosition(&x, &y); + return SetPositionAndSize(x, y, aWidth, aHeight, + aRepaint ? nsIBaseWindow::eRepaint : 0); +} + +NS_IMETHODIMP +nsDocShell::GetSize(int32_t* aWidth, int32_t* aHeight) { + return GetPositionAndSize(nullptr, nullptr, aWidth, aHeight); +} + +NS_IMETHODIMP +nsDocShell::SetPositionAndSize(int32_t aX, int32_t aY, int32_t aWidth, + int32_t aHeight, uint32_t aFlags) { + mBounds.SetRect(aX, aY, aWidth, aHeight); + + // Hold strong ref, since SetBounds can make us null out mContentViewer + nsCOMPtr<nsIContentViewer> viewer = mContentViewer; + if (viewer) { + uint32_t cvflags = (aFlags & nsIBaseWindow::eDelayResize) + ? nsIContentViewer::eDelayResize + : 0; + // XXX Border figured in here or is that handled elsewhere? + nsresult rv = viewer->SetBoundsWithFlags(mBounds, cvflags); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetPositionAndSize(int32_t* aX, int32_t* aY, int32_t* aWidth, + int32_t* aHeight) { + if (mParentWidget) { + // ensure size is up-to-date if window has changed resolution + LayoutDeviceIntRect r = mParentWidget->GetClientBounds(); + SetPositionAndSize(mBounds.X(), mBounds.Y(), r.Width(), r.Height(), 0); + } + + // We should really consider just getting this information from + // our window instead of duplicating the storage and code... + if (aWidth || aHeight) { + // Caller wants to know our size; make sure to give them up to + // date information. + RefPtr<Document> doc(do_GetInterface(GetAsSupports(mParent))); + if (doc) { + doc->FlushPendingNotifications(FlushType::Layout); + } + } + + DoGetPositionAndSize(aX, aY, aWidth, aHeight); + return NS_OK; +} + +void nsDocShell::DoGetPositionAndSize(int32_t* aX, int32_t* aY, int32_t* aWidth, + int32_t* aHeight) { + if (aX) { + *aX = mBounds.X(); + } + if (aY) { + *aY = mBounds.Y(); + } + if (aWidth) { + *aWidth = mBounds.Width(); + } + if (aHeight) { + *aHeight = mBounds.Height(); + } +} + +NS_IMETHODIMP +nsDocShell::SetDimensions(DimensionRequest&& aRequest) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShell::GetDimensions(DimensionKind aDimensionKind, int32_t* aX, + int32_t* aY, int32_t* aCX, int32_t* aCY) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShell::Repaint(bool aForce) { + PresShell* presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); + + RefPtr<nsViewManager> viewManager = presShell->GetViewManager(); + NS_ENSURE_TRUE(viewManager, NS_ERROR_FAILURE); + + viewManager->InvalidateAllViews(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetParentWidget(nsIWidget** aParentWidget) { + NS_ENSURE_ARG_POINTER(aParentWidget); + + *aParentWidget = mParentWidget; + NS_IF_ADDREF(*aParentWidget); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetParentWidget(nsIWidget* aParentWidget) { + MOZ_ASSERT(!mIsBeingDestroyed); + mParentWidget = aParentWidget; + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetParentNativeWindow(nativeWindow* aParentNativeWindow) { + NS_ENSURE_ARG_POINTER(aParentNativeWindow); + + if (mParentWidget) { + *aParentNativeWindow = mParentWidget->GetNativeData(NS_NATIVE_WIDGET); + } else { + *aParentNativeWindow = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetParentNativeWindow(nativeWindow aParentNativeWindow) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShell::GetNativeHandle(nsAString& aNativeHandle) { + // the nativeHandle should be accessed from nsIAppWindow + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShell::GetVisibility(bool* aVisibility) { + NS_ENSURE_ARG_POINTER(aVisibility); + + *aVisibility = false; + + if (!mContentViewer) { + return NS_OK; + } + + PresShell* presShell = GetPresShell(); + if (!presShell) { + return NS_OK; + } + + // get the view manager + nsViewManager* vm = presShell->GetViewManager(); + NS_ENSURE_TRUE(vm, NS_ERROR_FAILURE); + + // get the root view + nsView* view = vm->GetRootView(); // views are not ref counted + NS_ENSURE_TRUE(view, NS_ERROR_FAILURE); + + // if our root view is hidden, we are not visible + if (view->GetVisibility() == ViewVisibility::Hide) { + return NS_OK; + } + + // otherwise, we must walk up the document and view trees checking + // for a hidden view, unless we're an off screen browser, which + // would make this test meaningless. + + RefPtr<nsDocShell> docShell = this; + RefPtr<nsDocShell> parentItem = docShell->GetInProcessParentDocshell(); + while (parentItem) { + // Null-check for crash in bug 267804 + if (!parentItem->GetPresShell()) { + MOZ_ASSERT_UNREACHABLE("parent docshell has null pres shell"); + return NS_OK; + } + + vm = docShell->GetPresShell()->GetViewManager(); + if (vm) { + view = vm->GetRootView(); + } + + if (view) { + view = view->GetParent(); // anonymous inner view + if (view) { + view = view->GetParent(); // subdocumentframe's view + } + } + + nsIFrame* frame = view ? view->GetFrame() : nullptr; + if (frame && !frame->IsVisibleConsideringAncestors( + nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) { + return NS_OK; + } + + docShell = parentItem; + parentItem = docShell->GetInProcessParentDocshell(); + } + + nsCOMPtr<nsIBaseWindow> treeOwnerAsWin(do_QueryInterface(mTreeOwner)); + if (!treeOwnerAsWin) { + *aVisibility = true; + return NS_OK; + } + + // Check with the tree owner as well to give embedders a chance to + // expose visibility as well. + nsresult rv = treeOwnerAsWin->GetVisibility(aVisibility); + if (rv == NS_ERROR_NOT_IMPLEMENTED) { + // The tree owner had no opinion on our visibility. + *aVisibility = true; + return NS_OK; + } + return rv; +} + +void nsDocShell::ActivenessMaybeChanged() { + const bool isActive = mBrowsingContext->IsActive(); + if (RefPtr<PresShell> presShell = GetPresShell()) { + presShell->ActivenessMaybeChanged(); + } + + // Tell the window about it + if (mScriptGlobal) { + mScriptGlobal->SetIsBackground(!isActive); + if (RefPtr<Document> doc = mScriptGlobal->GetExtantDoc()) { + // Update orientation when the top-level browsing context becomes active. + if (isActive && mBrowsingContext->IsTop()) { + // We only care about the top-level browsing context. + auto orientation = mBrowsingContext->GetOrientationLock(); + ScreenOrientation::UpdateActiveOrientationLock(orientation); + } + + doc->PostVisibilityUpdateEvent(); + } + } + + // Tell the nsDOMNavigationTiming about it + RefPtr<nsDOMNavigationTiming> timing = mTiming; + if (!timing && mContentViewer) { + if (Document* doc = mContentViewer->GetDocument()) { + timing = doc->GetNavigationTiming(); + } + } + if (timing) { + timing->NotifyDocShellStateChanged( + isActive ? nsDOMNavigationTiming::DocShellState::eActive + : nsDOMNavigationTiming::DocShellState::eInactive); + } + + // Restart or stop meta refresh timers if necessary + if (mDisableMetaRefreshWhenInactive) { + if (isActive) { + ResumeRefreshURIs(); + } else { + SuspendRefreshURIs(); + } + } + + if (InputTaskManager::CanSuspendInputEvent()) { + mBrowsingContext->Group()->UpdateInputTaskManagerIfNeeded(isActive); + } +} + +NS_IMETHODIMP +nsDocShell::SetDefaultLoadFlags(uint32_t aDefaultLoadFlags) { + if (!mWillChangeProcess) { + // Intentionally ignoring handling discarded browsing contexts. + Unused << mBrowsingContext->SetDefaultLoadFlags(aDefaultLoadFlags); + } else { + // Bug 1623565: DevTools tries to clean up defaultLoadFlags on + // shutdown. Sorry DevTools, your DocShell is in another process. + NS_WARNING("nsDocShell::SetDefaultLoadFlags called on Zombie DocShell"); + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetDefaultLoadFlags(uint32_t* aDefaultLoadFlags) { + *aDefaultLoadFlags = mBrowsingContext->GetDefaultLoadFlags(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetFailedChannel(nsIChannel** aFailedChannel) { + NS_ENSURE_ARG_POINTER(aFailedChannel); + Document* doc = GetDocument(); + if (!doc) { + *aFailedChannel = nullptr; + return NS_OK; + } + NS_IF_ADDREF(*aFailedChannel = doc->GetFailedChannel()); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetVisibility(bool aVisibility) { + // Show()/Hide() may change mContentViewer. + nsCOMPtr<nsIContentViewer> cv = mContentViewer; + if (!cv) { + return NS_OK; + } + if (aVisibility) { + cv->Show(); + } else { + cv->Hide(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetEnabled(bool* aEnabled) { + NS_ENSURE_ARG_POINTER(aEnabled); + *aEnabled = true; + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShell::SetEnabled(bool aEnabled) { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +nsDocShell::GetMainWidget(nsIWidget** aMainWidget) { + // We don't create our own widget, so simply return the parent one. + return GetParentWidget(aMainWidget); +} + +NS_IMETHODIMP +nsDocShell::GetTitle(nsAString& aTitle) { + aTitle = mTitle; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetTitle(const nsAString& aTitle) { + // Avoid unnecessary updates of the title if the URI and the title haven't + // changed. + if (mTitleValidForCurrentURI && mTitle == aTitle) { + return NS_OK; + } + + // Store local title + mTitle = aTitle; + mTitleValidForCurrentURI = true; + + // When title is set on the top object it should then be passed to the + // tree owner. + if (mBrowsingContext->IsTop()) { + nsCOMPtr<nsIBaseWindow> treeOwnerAsWin(do_QueryInterface(mTreeOwner)); + if (treeOwnerAsWin) { + treeOwnerAsWin->SetTitle(aTitle); + } + } + + if (mCurrentURI && mLoadType != LOAD_ERROR_PAGE) { + UpdateGlobalHistoryTitle(mCurrentURI); + } + + // Update SessionHistory with the document's title. + if (mLoadType != LOAD_BYPASS_HISTORY && mLoadType != LOAD_ERROR_PAGE) { + SetTitleOnHistoryEntry(true); + } + + return NS_OK; +} + +void nsDocShell::SetTitleOnHistoryEntry(bool aUpdateEntryInSessionHistory) { + if (mOSHE) { + mOSHE->SetTitle(mTitle); + } + + if (mActiveEntry && mBrowsingContext) { + mActiveEntry->SetTitle(mTitle); + if (aUpdateEntryInSessionHistory) { + if (XRE_IsParentProcess()) { + SessionHistoryEntry* entry = + mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry(); + if (entry) { + entry->SetTitle(mTitle); + } + } else { + mozilla::Unused + << ContentChild::GetSingleton()->SendSessionHistoryEntryTitle( + mBrowsingContext, mTitle); + } + } + } +} + +nsPoint nsDocShell::GetCurScrollPos() { + nsPoint scrollPos; + if (nsIScrollableFrame* sf = GetRootScrollFrame()) { + scrollPos = sf->GetVisualViewportOffset(); + } + return scrollPos; +} + +nsresult nsDocShell::SetCurScrollPosEx(int32_t aCurHorizontalPos, + int32_t aCurVerticalPos) { + nsIScrollableFrame* sf = GetRootScrollFrame(); + NS_ENSURE_TRUE(sf, NS_ERROR_FAILURE); + + ScrollMode scrollMode = + sf->IsSmoothScroll() ? ScrollMode::SmoothMsd : ScrollMode::Instant; + + nsPoint targetPos(aCurHorizontalPos, aCurVerticalPos); + sf->ScrollTo(targetPos, scrollMode); + + // Set the visual viewport offset as well. + + RefPtr<PresShell> presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); + + nsPresContext* presContext = presShell->GetPresContext(); + NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE); + + // Only the root content document can have a distinct visual viewport offset. + if (!presContext->IsRootContentDocumentCrossProcess()) { + return NS_OK; + } + + // Not on a platform with a distinct visual viewport - don't bother setting + // the visual viewport offset. + if (!presShell->IsVisualViewportSizeSet()) { + return NS_OK; + } + + presShell->ScrollToVisual(targetPos, layers::FrameMetrics::eMainThread, + scrollMode); + + return NS_OK; +} + +void nsDocShell::SetScrollbarPreference(mozilla::ScrollbarPreference aPref) { + if (mScrollbarPref == aPref) { + return; + } + mScrollbarPref = aPref; + auto* ps = GetPresShell(); + if (!ps) { + return; + } + nsIFrame* scrollFrame = ps->GetRootScrollFrame(); + if (!scrollFrame) { + return; + } + ps->FrameNeedsReflow(scrollFrame, + IntrinsicDirty::FrameAncestorsAndDescendants, + NS_FRAME_IS_DIRTY); +} + +//***************************************************************************** +// nsDocShell::nsIRefreshURI +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::RefreshURI(nsIURI* aURI, nsIPrincipal* aPrincipal, + uint32_t aDelay) { + MOZ_ASSERT(!mIsBeingDestroyed); + + NS_ENSURE_ARG(aURI); + + /* Check if Meta refresh/redirects are permitted. Some + * embedded applications may not want to do this. + * Must do this before sending out NOTIFY_REFRESH events + * because listeners may have side effects (e.g. displaying a + * button to manually trigger the refresh later). + */ + bool allowRedirects = true; + GetAllowMetaRedirects(&allowRedirects); + if (!allowRedirects) { + return NS_OK; + } + + // If any web progress listeners are listening for NOTIFY_REFRESH events, + // give them a chance to block this refresh. + bool sameURI; + nsresult rv = aURI->Equals(mCurrentURI, &sameURI); + if (NS_FAILED(rv)) { + sameURI = false; + } + if (!RefreshAttempted(this, aURI, aDelay, sameURI)) { + return NS_OK; + } + + nsCOMPtr<nsITimerCallback> refreshTimer = + new nsRefreshTimer(this, aURI, aPrincipal, aDelay); + + BusyFlags busyFlags = GetBusyFlags(); + + if (!mRefreshURIList) { + mRefreshURIList = nsArray::Create(); + } + + if (busyFlags & BUSY_FLAGS_BUSY || + (!mBrowsingContext->IsActive() && mDisableMetaRefreshWhenInactive)) { + // We don't want to create the timer right now. Instead queue up the + // request and trigger the timer in EndPageLoad() or whenever we become + // active. + mRefreshURIList->AppendElement(refreshTimer); + } else { + // There is no page loading going on right now. Create the + // timer and fire it right away. + nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow(); + NS_ENSURE_TRUE(win, NS_ERROR_FAILURE); + + nsCOMPtr<nsITimer> timer; + MOZ_TRY_VAR(timer, NS_NewTimerWithCallback(refreshTimer, aDelay, + nsITimer::TYPE_ONE_SHOT)); + + mRefreshURIList->AppendElement(timer); // owning timer ref + } + return NS_OK; +} + +nsresult nsDocShell::ForceRefreshURIFromTimer(nsIURI* aURI, + nsIPrincipal* aPrincipal, + uint32_t aDelay, + nsITimer* aTimer) { + MOZ_ASSERT(aTimer, "Must have a timer here"); + + // Remove aTimer from mRefreshURIList if needed + if (mRefreshURIList) { + uint32_t n = 0; + mRefreshURIList->GetLength(&n); + + for (uint32_t i = 0; i < n; ++i) { + nsCOMPtr<nsITimer> timer = do_QueryElementAt(mRefreshURIList, i); + if (timer == aTimer) { + mRefreshURIList->RemoveElementAt(i); + break; + } + } + } + + return ForceRefreshURI(aURI, aPrincipal, aDelay); +} + +NS_IMETHODIMP +nsDocShell::ForceRefreshURI(nsIURI* aURI, nsIPrincipal* aPrincipal, + uint32_t aDelay) { + NS_ENSURE_ARG(aURI); + + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aURI); + loadState->SetOriginalURI(mCurrentURI); + loadState->SetResultPrincipalURI(aURI); + loadState->SetResultPrincipalURIIsSome(true); + loadState->SetKeepResultPrincipalURIIfSet(true); + loadState->SetIsMetaRefresh(true); + + // Set the triggering pricipal to aPrincipal if available, or current + // document's principal otherwise. + nsCOMPtr<nsIPrincipal> principal = aPrincipal; + RefPtr<Document> doc = GetDocument(); + if (!principal) { + if (!doc) { + return NS_ERROR_FAILURE; + } + principal = doc->NodePrincipal(); + } + loadState->SetTriggeringPrincipal(principal); + if (doc) { + loadState->SetCsp(doc->GetCsp()); + loadState->SetHasValidUserGestureActivation( + doc->HasValidTransientUserGestureActivation()); + loadState->SetTriggeringSandboxFlags(doc->GetSandboxFlags()); + } + + loadState->SetPrincipalIsExplicit(true); + + /* Check if this META refresh causes a redirection + * to another site. + */ + bool equalUri = false; + nsresult rv = aURI->Equals(mCurrentURI, &equalUri); + + nsCOMPtr<nsIReferrerInfo> referrerInfo; + if (NS_SUCCEEDED(rv) && !equalUri && aDelay <= REFRESH_REDIRECT_TIMER) { + /* It is a META refresh based redirection within the threshold time + * we have in mind (15000 ms as defined by REFRESH_REDIRECT_TIMER). + * Pass a REPLACE flag to LoadURI(). + */ + loadState->SetLoadType(LOAD_REFRESH_REPLACE); + + /* For redirects we mimic HTTP, which passes the + * original referrer. + * We will pass in referrer but will not send to server + */ + if (mReferrerInfo) { + referrerInfo = static_cast<ReferrerInfo*>(mReferrerInfo.get()) + ->CloneWithNewSendReferrer(false); + } + } else { + loadState->SetLoadType(LOAD_REFRESH); + /* We do need to pass in a referrer, but we don't want it to + * be sent to the server. + * For most refreshes the current URI is an appropriate + * internal referrer. + */ + referrerInfo = new ReferrerInfo(mCurrentURI, ReferrerPolicy::_empty, false); + } + + loadState->SetReferrerInfo(referrerInfo); + loadState->SetLoadFlags( + nsIWebNavigation::LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL); + loadState->SetFirstParty(true); + + /* + * LoadURI(...) will cancel all refresh timers... This causes the + * Timer and its refreshData instance to be released... + */ + LoadURI(loadState, false); + + return NS_OK; +} + +static const char16_t* SkipASCIIWhitespace(const char16_t* aStart, + const char16_t* aEnd) { + const char16_t* iter = aStart; + while (iter != aEnd && mozilla::IsAsciiWhitespace(*iter)) { + ++iter; + } + return iter; +} + +static std::tuple<const char16_t*, const char16_t*> ExtractURLString( + const char16_t* aPosition, const char16_t* aEnd) { + MOZ_ASSERT(aPosition != aEnd); + + // 1. Let urlString be the substring of input from the code point at + // position to the end of the string. + const char16_t* urlStart = aPosition; + const char16_t* urlEnd = aEnd; + + // 2. If the code point in input pointed to by position is U+0055 (U) or + // U+0075 (u), then advance position to the next code point. + // Otherwise, jump to the step labeled skip quotes. + if (*aPosition == 'U' || *aPosition == 'u') { + ++aPosition; + + // 3. If the code point in input pointed to by position is U+0052 (R) or + // U+0072 (r), then advance position to the next code point. + // Otherwise, jump to the step labeled parse. + if (aPosition == aEnd || (*aPosition != 'R' && *aPosition != 'r')) { + return std::make_tuple(urlStart, urlEnd); + } + + ++aPosition; + + // 4. If the code point in input pointed to by position is U+004C (L) or + // U+006C (l), then advance position to the next code point. + // Otherwise, jump to the step labeled parse. + if (aPosition == aEnd || (*aPosition != 'L' && *aPosition != 'l')) { + return std::make_tuple(urlStart, urlEnd); + } + + ++aPosition; + + // 5. Skip ASCII whitespace within input given position. + aPosition = SkipASCIIWhitespace(aPosition, aEnd); + + // 6. If the code point in input pointed to by position is U+003D (=), + // then advance position to the next code point. Otherwise, jump to + // the step labeled parse. + if (aPosition == aEnd || *aPosition != '=') { + return std::make_tuple(urlStart, urlEnd); + } + + ++aPosition; + + // 7. Skip ASCII whitespace within input given position. + aPosition = SkipASCIIWhitespace(aPosition, aEnd); + } + + // 8. Skip quotes: If the code point in input pointed to by position is + // U+0027 (') or U+0022 ("), then let quote be that code point, and + // advance position to the next code point. Otherwise, let quote be + // the empty string. + Maybe<char> quote; + if (aPosition != aEnd && (*aPosition == '\'' || *aPosition == '"')) { + quote.emplace(*aPosition); + ++aPosition; + } + + // 9. Set urlString to the substring of input from the code point at + // position to the end of the string. + urlStart = aPosition; + urlEnd = aEnd; + + // 10. If quote is not the empty string, and there is a code point in + // urlString equal to quote, then truncate urlString at that code + // point, so that it and all subsequent code points are removed. + const char16_t* quotePos; + if (quote.isSome() && + (quotePos = nsCharTraits<char16_t>::find( + urlStart, std::distance(urlStart, aEnd), quote.value()))) { + urlEnd = quotePos; + } + + return std::make_tuple(urlStart, urlEnd); +} + +void nsDocShell::SetupRefreshURIFromHeader(Document* aDocument, + const nsAString& aHeader) { + if (mIsBeingDestroyed) { + return; + } + + const char16_t* position = aHeader.BeginReading(); + const char16_t* end = aHeader.EndReading(); + + // See + // https://html.spec.whatwg.org/#pragma-directives:shared-declarative-refresh-steps. + + // 3. Skip ASCII whitespace + position = SkipASCIIWhitespace(position, end); + + // 4. Let time be 0. + CheckedInt<uint32_t> milliSeconds; + + // 5. Collect a sequence of code points that are ASCII digits + const char16_t* digitsStart = position; + while (position != end && mozilla::IsAsciiDigit(*position)) { + ++position; + } + + if (position == digitsStart) { + // 6. If timeString is the empty string, then: + // 1. If the code point in input pointed to by position is not U+002E + // (.), then return. + if (position == end || *position != '.') { + return; + } + } else { + // 7. Otherwise, set time to the result of parsing timeString using the + // rules for parsing non-negative integers. + nsContentUtils::ParseHTMLIntegerResultFlags result; + uint32_t seconds = + nsContentUtils::ParseHTMLInteger(digitsStart, position, &result); + MOZ_ASSERT(!(result & nsContentUtils::eParseHTMLInteger_Negative)); + if (result & nsContentUtils::eParseHTMLInteger_Error) { + // The spec assumes no errors here (since we only pass ASCII digits in), + // but we can still overflow, so this block should deal with that (and + // only that). + MOZ_ASSERT(!(result & nsContentUtils::eParseHTMLInteger_ErrorOverflow)); + return; + } + MOZ_ASSERT( + !(result & nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput)); + + milliSeconds = seconds; + milliSeconds *= 1000; + if (!milliSeconds.isValid()) { + return; + } + } + + // 8. Collect a sequence of code points that are ASCII digits and U+002E FULL + // STOP characters (.) from input given position. Ignore any collected + // characters. + while (position != end && + (mozilla::IsAsciiDigit(*position) || *position == '.')) { + ++position; + } + + // 9. Let urlRecord be document's URL. + nsCOMPtr<nsIURI> urlRecord(aDocument->GetDocumentURI()); + + // 10. If position is not past the end of input + if (position != end) { + // 1. If the code point in input pointed to by position is not U+003B (;), + // U+002C (,), or ASCII whitespace, then return. + if (*position != ';' && *position != ',' && + !mozilla::IsAsciiWhitespace(*position)) { + return; + } + + // 2. Skip ASCII whitespace within input given position. + position = SkipASCIIWhitespace(position, end); + + // 3. If the code point in input pointed to by position is U+003B (;) or + // U+002C (,), then advance position to the next code point. + if (position != end && (*position == ';' || *position == ',')) { + ++position; + + // 4. Skip ASCII whitespace within input given position. + position = SkipASCIIWhitespace(position, end); + } + + // 11. If position is not past the end of input, then: + if (position != end) { + const char16_t* urlStart; + const char16_t* urlEnd; + + // 1-10. See ExtractURLString. + std::tie(urlStart, urlEnd) = ExtractURLString(position, end); + + // 11. Parse: Parse urlString relative to document. If that fails, return. + // Otherwise, set urlRecord to the resulting URL record. + nsresult rv = + NS_NewURI(getter_AddRefs(urlRecord), + Substring(urlStart, std::distance(urlStart, urlEnd)), + /* charset = */ nullptr, aDocument->GetDocBaseURI()); + NS_ENSURE_SUCCESS_VOID(rv); + } + } + + nsIPrincipal* principal = aDocument->NodePrincipal(); + nsCOMPtr<nsIScriptSecurityManager> securityManager = + nsContentUtils::GetSecurityManager(); + nsresult rv = securityManager->CheckLoadURIWithPrincipal( + principal, urlRecord, + nsIScriptSecurityManager::LOAD_IS_AUTOMATIC_DOCUMENT_REPLACEMENT, + aDocument->InnerWindowID()); + NS_ENSURE_SUCCESS_VOID(rv); + + bool isjs = true; + rv = NS_URIChainHasFlags( + urlRecord, nsIProtocolHandler::URI_OPENING_EXECUTES_SCRIPT, &isjs); + NS_ENSURE_SUCCESS_VOID(rv); + + if (isjs) { + return; + } + + RefreshURI(urlRecord, principal, milliSeconds.value()); +} + +static void DoCancelRefreshURITimers(nsIMutableArray* aTimerList) { + if (!aTimerList) { + return; + } + + uint32_t n = 0; + aTimerList->GetLength(&n); + + while (n) { + nsCOMPtr<nsITimer> timer(do_QueryElementAt(aTimerList, --n)); + + aTimerList->RemoveElementAt(n); // bye bye owning timer ref + + if (timer) { + timer->Cancel(); + } + } +} + +NS_IMETHODIMP +nsDocShell::CancelRefreshURITimers() { + DoCancelRefreshURITimers(mRefreshURIList); + DoCancelRefreshURITimers(mSavedRefreshURIList); + DoCancelRefreshURITimers(mBFCachedRefreshURIList); + mRefreshURIList = nullptr; + mSavedRefreshURIList = nullptr; + mBFCachedRefreshURIList = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetRefreshPending(bool* aResult) { + if (!mRefreshURIList) { + *aResult = false; + return NS_OK; + } + + uint32_t count; + nsresult rv = mRefreshURIList->GetLength(&count); + if (NS_SUCCEEDED(rv)) { + *aResult = (count != 0); + } + return rv; +} + +void nsDocShell::RefreshURIToQueue() { + if (mRefreshURIList) { + uint32_t n = 0; + mRefreshURIList->GetLength(&n); + + for (uint32_t i = 0; i < n; ++i) { + nsCOMPtr<nsITimer> timer = do_QueryElementAt(mRefreshURIList, i); + if (!timer) { + continue; // this must be a nsRefreshURI already + } + + // Replace this timer object with a nsRefreshTimer object. + nsCOMPtr<nsITimerCallback> callback; + timer->GetCallback(getter_AddRefs(callback)); + + timer->Cancel(); + + mRefreshURIList->ReplaceElementAt(callback, i); + } + } +} + +NS_IMETHODIMP +nsDocShell::SuspendRefreshURIs() { + RefreshURIToQueue(); + + // Suspend refresh URIs for our child shells as well. + for (auto* child : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShell> shell = do_QueryObject(child); + if (shell) { + shell->SuspendRefreshURIs(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::ResumeRefreshURIs() { + RefreshURIFromQueue(); + + // Resume refresh URIs for our child shells as well. + for (auto* child : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShell> shell = do_QueryObject(child); + if (shell) { + shell->ResumeRefreshURIs(); + } + } + + return NS_OK; +} + +nsresult nsDocShell::RefreshURIFromQueue() { + if (!mRefreshURIList) { + return NS_OK; + } + uint32_t n = 0; + mRefreshURIList->GetLength(&n); + + while (n) { + nsCOMPtr<nsITimerCallback> refreshInfo = + do_QueryElementAt(mRefreshURIList, --n); + + if (refreshInfo) { + // This is the nsRefreshTimer object, waiting to be + // setup in a timer object and fired. + // Create the timer and trigger it. + uint32_t delay = static_cast<nsRefreshTimer*>( + static_cast<nsITimerCallback*>(refreshInfo)) + ->GetDelay(); + nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow(); + if (win) { + nsCOMPtr<nsITimer> timer; + NS_NewTimerWithCallback(getter_AddRefs(timer), refreshInfo, delay, + nsITimer::TYPE_ONE_SHOT); + + if (timer) { + // Replace the nsRefreshTimer element in the queue with + // its corresponding timer object, so that in case another + // load comes through before the timer can go off, the timer will + // get cancelled in CancelRefreshURITimer() + mRefreshURIList->ReplaceElementAt(timer, n); + } + } + } + } + + return NS_OK; +} + +static bool IsFollowupPartOfMultipart(nsIRequest* aRequest) { + nsCOMPtr<nsIMultiPartChannel> multiPartChannel = do_QueryInterface(aRequest); + bool firstPart = false; + return multiPartChannel && + NS_SUCCEEDED(multiPartChannel->GetIsFirstPart(&firstPart)) && + !firstPart; +} + +nsresult nsDocShell::Embed(nsIContentViewer* aContentViewer, + WindowGlobalChild* aWindowActor, + bool aIsTransientAboutBlank, bool aPersist, + nsIRequest* aRequest, nsIURI* aPreviousURI) { + // Save the LayoutHistoryState of the previous document, before + // setting up new document + PersistLayoutHistoryState(); + + nsresult rv = SetupNewViewer(aContentViewer, aWindowActor); + NS_ENSURE_SUCCESS(rv, rv); + + // XXX What if SetupNewViewer fails? + if (mozilla::SessionHistoryInParent() ? !!mLoadingEntry : !!mLSHE) { + // Set history.state + SetDocCurrentStateObj(mLSHE, + mLoadingEntry ? &mLoadingEntry->mInfo : nullptr); + } + + if (mLSHE) { + // Restore the editing state, if it's stored in session history. + if (mLSHE->HasDetachedEditor()) { + ReattachEditorToWindow(mLSHE); + } + + SetHistoryEntryAndUpdateBC(Nothing(), Some<nsISHEntry*>(mLSHE)); + } + + if (!aIsTransientAboutBlank && mozilla::SessionHistoryInParent() && + !IsFollowupPartOfMultipart(aRequest)) { + bool expired = false; + uint32_t cacheKey = 0; + nsCOMPtr<nsICacheInfoChannel> cacheChannel = do_QueryInterface(aRequest); + if (cacheChannel) { + // Check if the page has expired from cache + uint32_t expTime = 0; + cacheChannel->GetCacheTokenExpirationTime(&expTime); + uint32_t now = PRTimeToSeconds(PR_Now()); + if (expTime <= now) { + expired = true; + } + + // The checks for updating cache key are similar to the old session + // history in OnNewURI. Try to update the cache key if + // - we should update session history and aren't doing a session + // history load. + // - we're doing a forced reload. + if (((!mLoadingEntry || !mLoadingEntry->mLoadIsFromSessionHistory) && + mBrowsingContext->ShouldUpdateSessionHistory(mLoadType)) || + IsForceReloadType(mLoadType)) { + cacheChannel->GetCacheKey(&cacheKey); + } + } + + MOZ_LOG(gSHLog, LogLevel::Debug, ("document %p Embed", this)); + MoveLoadingToActiveEntry(aPersist, expired, cacheKey, aPreviousURI); + } + + bool updateHistory = true; + + // Determine if this type of load should update history + switch (mLoadType) { + case LOAD_NORMAL_REPLACE: + case LOAD_REFRESH_REPLACE: + case LOAD_STOP_CONTENT_AND_REPLACE: + case LOAD_RELOAD_BYPASS_CACHE: + case LOAD_RELOAD_BYPASS_PROXY: + case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE: + case LOAD_REPLACE_BYPASS_CACHE: + updateHistory = false; + break; + default: + break; + } + + if (!updateHistory) { + SetLayoutHistoryState(nullptr); + } + + return NS_OK; +} + +//***************************************************************************** +// nsDocShell::nsIWebProgressListener +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::OnProgressChange(nsIWebProgress* aProgress, nsIRequest* aRequest, + int32_t aCurSelfProgress, int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::OnStateChange(nsIWebProgress* aProgress, nsIRequest* aRequest, + uint32_t aStateFlags, nsresult aStatus) { + if ((~aStateFlags & (STATE_START | STATE_IS_NETWORK)) == 0) { + // Save timing statistics. + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + nsAutoCString aURI; + uri->GetAsciiSpec(aURI); + + if (this == aProgress) { + mozilla::Unused << MaybeInitTiming(); + mTiming->NotifyFetchStart(uri, + ConvertLoadTypeToNavigationType(mLoadType)); + // If we are starting a DocumentChannel, we need to pass the timing + // statistics so that should a process switch occur, the starting type can + // be passed to the new DocShell running in the other content process. + if (RefPtr<DocumentChannel> docChannel = do_QueryObject(aRequest)) { + docChannel->SetNavigationTiming(mTiming); + } + } + + // Page has begun to load + mBusyFlags = (BusyFlags)(BUSY_FLAGS_BUSY | BUSY_FLAGS_BEFORE_PAGE_LOAD); + + if ((aStateFlags & STATE_RESTORING) == 0) { + // Show the progress cursor if the pref is set + if (StaticPrefs::ui_use_activity_cursor()) { + nsCOMPtr<nsIWidget> mainWidget; + GetMainWidget(getter_AddRefs(mainWidget)); + if (mainWidget) { + mainWidget->SetCursor(nsIWidget::Cursor{eCursor_spinning}); + } + } + + if (StaticPrefs::browser_sessionstore_platform_collection_AtStartup()) { + if (IsForceReloadType(mLoadType)) { + if (WindowContext* windowContext = + mBrowsingContext->GetCurrentWindowContext()) { + SessionStoreChild::From(windowContext->GetWindowGlobalChild()) + ->ResetSessionStore(mBrowsingContext, + mBrowsingContext->GetSessionStoreEpoch()); + } + } + } + } + } else if ((~aStateFlags & (STATE_TRANSFERRING | STATE_IS_DOCUMENT)) == 0) { + // Page is loading + mBusyFlags = (BusyFlags)(BUSY_FLAGS_BUSY | BUSY_FLAGS_PAGE_LOADING); + } else if ((aStateFlags & STATE_STOP) && (aStateFlags & STATE_IS_NETWORK)) { + // Page has finished loading + mBusyFlags = BUSY_FLAGS_NONE; + + // Hide the progress cursor if the pref is set + if (StaticPrefs::ui_use_activity_cursor()) { + nsCOMPtr<nsIWidget> mainWidget; + GetMainWidget(getter_AddRefs(mainWidget)); + if (mainWidget) { + mainWidget->SetCursor(nsIWidget::Cursor{eCursor_standard}); + } + } + } + + if ((~aStateFlags & (STATE_IS_DOCUMENT | STATE_STOP)) == 0) { + nsCOMPtr<nsIWebProgress> webProgress = + do_QueryInterface(GetAsSupports(this)); + // Is the document stop notification for this document? + if (aProgress == webProgress.get()) { + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + EndPageLoad(aProgress, channel, aStatus); + } + } + // note that redirect state changes will go through here as well, but it + // is better to handle those in OnRedirectStateChange where more + // information is available. + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::OnLocationChange(nsIWebProgress* aProgress, nsIRequest* aRequest, + nsIURI* aURI, uint32_t aFlags) { + // Since we've now changed Documents, notify the BrowsingContext that we've + // changed. Ideally we'd just let the BrowsingContext do this when it + // changes the current window global, but that happens before this and we + // have a lot of tests that depend on the specific ordering of messages. + bool isTopLevel = false; + if (XRE_IsParentProcess() && + !(aFlags & nsIWebProgressListener::LOCATION_CHANGE_SAME_DOCUMENT) && + NS_SUCCEEDED(aProgress->GetIsTopLevel(&isTopLevel)) && isTopLevel) { + GetBrowsingContext()->Canonical()->UpdateSecurityState(); + } + return NS_OK; +} + +void nsDocShell::OnRedirectStateChange(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, + uint32_t aRedirectFlags, + uint32_t aStateFlags) { + NS_ASSERTION(aStateFlags & STATE_REDIRECTING, + "Calling OnRedirectStateChange when there is no redirect"); + + if (!(aStateFlags & STATE_IS_DOCUMENT)) { + return; // not a toplevel document + } + + nsCOMPtr<nsIURI> oldURI, newURI; + aOldChannel->GetURI(getter_AddRefs(oldURI)); + aNewChannel->GetURI(getter_AddRefs(newURI)); + if (!oldURI || !newURI) { + return; + } + + // DocumentChannel adds redirect chain to global history in the parent + // process. The redirect chain can't be queried from the content process, so + // there's no need to update global history here. + RefPtr<DocumentChannel> docChannel = do_QueryObject(aOldChannel); + if (!docChannel) { + // Below a URI visit is saved (see AddURIVisit method doc). + // The visit chain looks something like: + // ... + // Site N - 1 + // => Site N + // (redirect to =>) Site N + 1 (we are here!) + + // Get N - 1 and transition type + nsCOMPtr<nsIURI> previousURI; + uint32_t previousFlags = 0; + ExtractLastVisit(aOldChannel, getter_AddRefs(previousURI), &previousFlags); + + if (aRedirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL || + net::ChannelIsPost(aOldChannel)) { + // 1. Internal redirects are ignored because they are specific to the + // channel implementation. + // 2. POSTs are not saved by global history. + // + // Regardless, we need to propagate the previous visit to the new + // channel. + SaveLastVisit(aNewChannel, previousURI, previousFlags); + } else { + // Get the HTTP response code, if available. + uint32_t responseStatus = 0; + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aOldChannel); + if (httpChannel) { + Unused << httpChannel->GetResponseStatus(&responseStatus); + } + + // Add visit N -1 => N + AddURIVisit(oldURI, previousURI, previousFlags, responseStatus); + + // Since N + 1 could be the final destination, we will not save N => N + 1 + // here. OnNewURI will do that, so we will cache it. + SaveLastVisit(aNewChannel, oldURI, aRedirectFlags); + } + } + + if (!(aRedirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL) && + mLoadType & (LOAD_CMD_RELOAD | LOAD_CMD_HISTORY)) { + mLoadType = LOAD_NORMAL_REPLACE; + SetHistoryEntryAndUpdateBC(Some(nullptr), Nothing()); + } +} + +NS_IMETHODIMP +nsDocShell::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsresult aStatus, const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +already_AddRefed<nsIURIFixupInfo> nsDocShell::KeywordToURI( + const nsACString& aKeyword, bool aIsPrivateContext) { + nsCOMPtr<nsIURIFixupInfo> info; + if (!XRE_IsContentProcess()) { + nsCOMPtr<nsIURIFixup> uriFixup = components::URIFixup::Service(); + if (uriFixup) { + uriFixup->KeywordToURI(aKeyword, aIsPrivateContext, getter_AddRefs(info)); + } + } + return info.forget(); +} + +/* static */ +already_AddRefed<nsIURI> nsDocShell::MaybeFixBadCertDomainErrorURI( + nsIChannel* aChannel, nsIURI* aUrl) { + if (!aChannel) { + return nullptr; + } + + nsresult rv = NS_OK; + nsAutoCString host; + rv = aUrl->GetAsciiHost(host); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + // No point in going further if "www." is included in the hostname + // already. That is the only hueristic we're applying in this function. + if (StringBeginsWith(host, "www."_ns)) { + return nullptr; + } + + // Return if fixup enable pref is turned off. + if (!mozilla::StaticPrefs::security_bad_cert_domain_error_url_fix_enabled()) { + return nullptr; + } + + // Return if scheme is not HTTPS. + if (!SchemeIsHTTPS(aUrl)) { + return nullptr; + } + + nsCOMPtr<nsILoadInfo> info = aChannel->LoadInfo(); + if (!info) { + return nullptr; + } + + // Skip doing the fixup if our channel was redirected, because we + // shouldn't be guessing things about the post-redirect URI. + if (!info->RedirectChain().IsEmpty()) { + return nullptr; + } + + int32_t port = 0; + rv = aUrl->GetPort(&port); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + // Don't fix up hosts with ports. + if (port != -1) { + return nullptr; + } + + // Don't fix up localhost url. + if (host == "localhost") { + return nullptr; + } + + // Don't fix up hostnames with IP address. + if (net_IsValidIPv4Addr(host) || net_IsValidIPv6Addr(host)) { + return nullptr; + } + + nsAutoCString userPass; + rv = aUrl->GetUserPass(userPass); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + // Security - URLs with user / password info should NOT be modified. + if (!userPass.IsEmpty()) { + return nullptr; + } + + nsCOMPtr<nsITransportSecurityInfo> tsi; + rv = aChannel->GetSecurityInfo(getter_AddRefs(tsi)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + if (NS_WARN_IF(!tsi)) { + return nullptr; + } + + nsCOMPtr<nsIX509Cert> cert; + rv = tsi->GetServerCert(getter_AddRefs(cert)); + if (NS_WARN_IF(NS_FAILED(rv) || !cert)) { + return nullptr; + } + + nsTArray<uint8_t> certBytes; + rv = cert->GetRawDER(certBytes); + if (NS_FAILED(rv)) { + return nullptr; + } + + mozilla::pkix::Input serverCertInput; + mozilla::pkix::Result rv1 = + serverCertInput.Init(certBytes.Elements(), certBytes.Length()); + if (rv1 != mozilla::pkix::Success) { + return nullptr; + } + + nsAutoCString newHost("www."_ns); + newHost.Append(host); + + mozilla::pkix::Input newHostInput; + rv1 = newHostInput.Init( + BitwiseCast<const uint8_t*, const char*>(newHost.BeginReading()), + newHost.Length()); + if (rv1 != mozilla::pkix::Success) { + return nullptr; + } + + // Check if adding a "www." prefix to the request's hostname will + // cause the response's certificate to match. + rv1 = mozilla::pkix::CheckCertHostname(serverCertInput, newHostInput); + if (rv1 != mozilla::pkix::Success) { + return nullptr; + } + + nsCOMPtr<nsIURI> newURI; + Unused << NS_MutateURI(aUrl).SetHost(newHost).Finalize( + getter_AddRefs(newURI)); + + return newURI.forget(); +} + +/* static */ +already_AddRefed<nsIURI> nsDocShell::AttemptURIFixup( + nsIChannel* aChannel, nsresult aStatus, + const mozilla::Maybe<nsCString>& aOriginalURIString, uint32_t aLoadType, + bool aIsTopFrame, bool aAllowKeywordFixup, bool aUsePrivateBrowsing, + bool aNotifyKeywordSearchLoading, nsIInputStream** aNewPostData) { + if (aStatus != NS_ERROR_UNKNOWN_HOST && aStatus != NS_ERROR_NET_RESET && + aStatus != NS_ERROR_CONNECTION_REFUSED && + aStatus != + mozilla::psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERT_DOMAIN)) { + return nullptr; + } + + if (!(aLoadType == LOAD_NORMAL && aIsTopFrame) && !aAllowKeywordFixup) { + return nullptr; + } + + nsCOMPtr<nsIURI> url; + nsresult rv = aChannel->GetURI(getter_AddRefs(url)); + if (NS_FAILED(rv)) { + return nullptr; + } + + // + // Try and make an alternative URI from the old one + // + nsCOMPtr<nsIURI> newURI; + nsCOMPtr<nsIInputStream> newPostData; + + nsAutoCString oldSpec; + url->GetSpec(oldSpec); + + // + // First try keyword fixup + // + nsAutoString keywordProviderName, keywordAsSent; + if (aStatus == NS_ERROR_UNKNOWN_HOST && aAllowKeywordFixup) { + // we should only perform a keyword search under the following + // conditions: + // (0) Pref keyword.enabled is true + // (1) the url scheme is http (or https) + // (2) the url does not have a protocol scheme + // If we don't enforce such a policy, then we end up doing + // keyword searchs on urls we don't intend like imap, file, + // mailbox, etc. This could lead to a security problem where we + // send data to the keyword server that we shouldn't be. + // Someone needs to clean up keywords in general so we can + // determine on a per url basis if we want keywords + // enabled...this is just a bandaid... + nsAutoCString scheme; + Unused << url->GetScheme(scheme); + if (Preferences::GetBool("keyword.enabled", false) && + StringBeginsWith(scheme, "http"_ns)) { + bool attemptFixup = false; + nsAutoCString host; + Unused << url->GetHost(host); + if (host.FindChar('.') == kNotFound) { + attemptFixup = true; + } else { + // For domains with dots, we check the public suffix validity. + nsCOMPtr<nsIEffectiveTLDService> tldService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + if (tldService) { + nsAutoCString suffix; + attemptFixup = + NS_SUCCEEDED(tldService->GetKnownPublicSuffix(url, suffix)) && + suffix.IsEmpty(); + } + } + if (attemptFixup) { + nsCOMPtr<nsIURIFixupInfo> info; + // only send non-qualified hosts to the keyword server + if (aOriginalURIString && !aOriginalURIString->IsEmpty()) { + info = KeywordToURI(*aOriginalURIString, aUsePrivateBrowsing); + } else { + // + // If this string was passed through nsStandardURL by + // chance, then it may have been converted from UTF-8 to + // ACE, which would result in a completely bogus keyword + // query. Here we try to recover the original Unicode + // value, but this is not 100% correct since the value may + // have been normalized per the IDN normalization rules. + // + // Since we don't have access to the exact original string + // that was entered by the user, this will just have to do. + bool isACE; + nsAutoCString utf8Host; + nsCOMPtr<nsIIDNService> idnSrv = + do_GetService(NS_IDNSERVICE_CONTRACTID); + if (idnSrv && NS_SUCCEEDED(idnSrv->IsACE(host, &isACE)) && isACE && + NS_SUCCEEDED(idnSrv->ConvertACEtoUTF8(host, utf8Host))) { + info = KeywordToURI(utf8Host, aUsePrivateBrowsing); + + } else { + info = KeywordToURI(host, aUsePrivateBrowsing); + } + } + if (info) { + info->GetPreferredURI(getter_AddRefs(newURI)); + if (newURI) { + info->GetKeywordAsSent(keywordAsSent); + info->GetKeywordProviderName(keywordProviderName); + info->GetPostData(getter_AddRefs(newPostData)); + } + } + } + } + } + + // + // Now try change the address, e.g. turn http://foo into + // http://www.foo.com, and if that doesn't work try https with + // https://foo and https://www.foo.com. + // + if (aStatus == NS_ERROR_UNKNOWN_HOST || aStatus == NS_ERROR_NET_RESET) { + // Skip fixup for anything except a normal document load + // operation on the topframe. + bool doCreateAlternate = aLoadType == LOAD_NORMAL && aIsTopFrame; + + if (doCreateAlternate) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsIPrincipal* principal = loadInfo->TriggeringPrincipal(); + // Only do this if our channel was loaded directly by the user from the + // URL bar or similar (system principal) and not redirected, because we + // shouldn't be guessing things about links from other sites, or a + // post-redirect URI. + doCreateAlternate = principal && principal->IsSystemPrincipal() && + loadInfo->RedirectChain().IsEmpty(); + } + // Test if keyword lookup produced a new URI or not + if (doCreateAlternate && newURI) { + bool sameURI = false; + url->Equals(newURI, &sameURI); + if (!sameURI) { + // Keyword lookup made a new URI so no need to try + // an alternate one. + doCreateAlternate = false; + } + } + if (doCreateAlternate) { + newURI = nullptr; + newPostData = nullptr; + keywordProviderName.Truncate(); + keywordAsSent.Truncate(); + nsCOMPtr<nsIURIFixup> uriFixup = components::URIFixup::Service(); + if (uriFixup) { + nsCOMPtr<nsIURIFixupInfo> fixupInfo; + uriFixup->GetFixupURIInfo(oldSpec, + nsIURIFixup::FIXUP_FLAGS_MAKE_ALTERNATE_URI, + getter_AddRefs(fixupInfo)); + if (fixupInfo) { + fixupInfo->GetPreferredURI(getter_AddRefs(newURI)); + } + } + } + } else if (aStatus == NS_ERROR_CONNECTION_REFUSED && + Preferences::GetBool("browser.fixup.fallback-to-https", false)) { + // Try HTTPS, since http didn't work + if (SchemeIsHTTP(url)) { + int32_t port = 0; + url->GetPort(&port); + + // Fall back to HTTPS only if port is default + if (port == -1) { + newURI = nullptr; + newPostData = nullptr; + Unused << NS_MutateURI(url) + .SetScheme("https"_ns) + .Finalize(getter_AddRefs(newURI)); + } + } + } + + // If we have a SSL_ERROR_BAD_CERT_DOMAIN error, try prefixing the domain name + // with www. to see if we can avoid showing the cert error page. For example, + // https://example.com -> https://www.example.com. + if (aStatus == + mozilla::psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERT_DOMAIN)) { + newPostData = nullptr; + newURI = MaybeFixBadCertDomainErrorURI(aChannel, url); + } + + // Did we make a new URI that is different to the old one? If so + // load it. + // + if (newURI) { + // Make sure the new URI is different from the old one, + // otherwise there's little point trying to load it again. + bool sameURI = false; + url->Equals(newURI, &sameURI); + if (!sameURI) { + if (aNewPostData) { + newPostData.forget(aNewPostData); + } + if (aNotifyKeywordSearchLoading) { + // This notification is meant for Firefox Health Report so it + // can increment counts from the search engine + MaybeNotifyKeywordSearchLoading(keywordProviderName, keywordAsSent); + } + return newURI.forget(); + } + } + + return nullptr; +} + +nsresult nsDocShell::FilterStatusForErrorPage( + nsresult aStatus, nsIChannel* aChannel, uint32_t aLoadType, + bool aIsTopFrame, bool aUseErrorPages, bool aIsInitialDocument, + bool* aSkippedUnknownProtocolNavigation) { + // Errors to be shown only on top-level frames + if ((aStatus == NS_ERROR_UNKNOWN_HOST || + aStatus == NS_ERROR_CONNECTION_REFUSED || + aStatus == NS_ERROR_UNKNOWN_PROXY_HOST || + aStatus == NS_ERROR_PROXY_CONNECTION_REFUSED || + aStatus == NS_ERROR_PROXY_FORBIDDEN || + aStatus == NS_ERROR_PROXY_NOT_IMPLEMENTED || + aStatus == NS_ERROR_PROXY_AUTHENTICATION_FAILED || + aStatus == NS_ERROR_PROXY_TOO_MANY_REQUESTS || + aStatus == NS_ERROR_MALFORMED_URI || + aStatus == NS_ERROR_BLOCKED_BY_POLICY || + aStatus == NS_ERROR_DOM_COOP_FAILED || + aStatus == NS_ERROR_DOM_COEP_FAILED) && + (aIsTopFrame || aUseErrorPages)) { + return aStatus; + } + + if (aStatus == NS_ERROR_NET_TIMEOUT || + aStatus == NS_ERROR_NET_TIMEOUT_EXTERNAL || + aStatus == NS_ERROR_PROXY_GATEWAY_TIMEOUT || + aStatus == NS_ERROR_REDIRECT_LOOP || + aStatus == NS_ERROR_UNKNOWN_SOCKET_TYPE || + aStatus == NS_ERROR_NET_INTERRUPT || aStatus == NS_ERROR_NET_RESET || + aStatus == NS_ERROR_PROXY_BAD_GATEWAY || aStatus == NS_ERROR_OFFLINE || + aStatus == NS_ERROR_MALWARE_URI || aStatus == NS_ERROR_PHISHING_URI || + aStatus == NS_ERROR_UNWANTED_URI || aStatus == NS_ERROR_HARMFUL_URI || + aStatus == NS_ERROR_UNSAFE_CONTENT_TYPE || + aStatus == NS_ERROR_INTERCEPTION_FAILED || + aStatus == NS_ERROR_NET_INADEQUATE_SECURITY || + aStatus == NS_ERROR_NET_HTTP2_SENT_GOAWAY || + aStatus == NS_ERROR_NET_HTTP3_PROTOCOL_ERROR || + aStatus == NS_ERROR_DOM_BAD_URI || aStatus == NS_ERROR_FILE_NOT_FOUND || + aStatus == NS_ERROR_FILE_ACCESS_DENIED || + aStatus == NS_ERROR_CORRUPTED_CONTENT || + aStatus == NS_ERROR_INVALID_CONTENT_ENCODING || + NS_ERROR_GET_MODULE(aStatus) == NS_ERROR_MODULE_SECURITY) { + // Errors to be shown for any frame + return aStatus; + } + + if (aStatus == NS_ERROR_UNKNOWN_PROTOCOL) { + // For unknown protocols we only display an error if the load is triggered + // by the browser itself, or we're replacing the initial document (and + // nothing else). Showing the error for page-triggered navigations causes + // annoying behavior for users, see bug 1528305. + // + // We could, maybe, try to detect if this is in response to some user + // interaction (like clicking a link, or something else) and maybe show + // the error page in that case. But this allows for ctrl+clicking and such + // to see the error page. + nsCOMPtr<nsILoadInfo> info = aChannel->LoadInfo(); + if (!info->TriggeringPrincipal()->IsSystemPrincipal() && + StaticPrefs::dom_no_unknown_protocol_error_enabled() && + !aIsInitialDocument) { + if (aSkippedUnknownProtocolNavigation) { + *aSkippedUnknownProtocolNavigation = true; + } + return NS_OK; + } + return aStatus; + } + + if (aStatus == NS_ERROR_DOCUMENT_NOT_CACHED) { + // Non-caching channels will simply return NS_ERROR_OFFLINE. + // Caching channels would have to look at their flags to work + // out which error to return. Or we can fix up the error here. + if (!(aLoadType & LOAD_CMD_HISTORY)) { + return NS_ERROR_OFFLINE; + } + return aStatus; + } + + return NS_OK; +} + +nsresult nsDocShell::EndPageLoad(nsIWebProgress* aProgress, + nsIChannel* aChannel, nsresult aStatus) { + MOZ_LOG(gDocShellLeakLog, LogLevel::Debug, + ("DOCSHELL %p EndPageLoad status: %" PRIx32 "\n", this, + static_cast<uint32_t>(aStatus))); + if (!aChannel) { + return NS_ERROR_NULL_POINTER; + } + + // Make sure to discard the initial client if we never created the initial + // about:blank document. Do this before possibly returning from the method + // due to an error. + mInitialClientSource.reset(); + + nsCOMPtr<nsIConsoleReportCollector> reporter = do_QueryInterface(aChannel); + if (reporter) { + nsCOMPtr<nsILoadGroup> loadGroup; + aChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (loadGroup) { + reporter->FlushConsoleReports(loadGroup); + } else { + reporter->FlushConsoleReports(GetDocument()); + } + } + + nsCOMPtr<nsIURI> url; + nsresult rv = aChannel->GetURI(getter_AddRefs(url)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsITimedChannel> timingChannel = do_QueryInterface(aChannel); + if (timingChannel) { + TimeStamp channelCreationTime; + rv = timingChannel->GetChannelCreation(&channelCreationTime); + if (NS_SUCCEEDED(rv) && !channelCreationTime.IsNull()) { + Telemetry::AccumulateTimeDelta(Telemetry::TOTAL_CONTENT_PAGE_LOAD_TIME, + channelCreationTime); + } + } + + // Timing is picked up by the window, we don't need it anymore + mTiming = nullptr; + + // clean up reload state for meta charset + if (eCharsetReloadRequested == mCharsetReloadState) { + mCharsetReloadState = eCharsetReloadStopOrigional; + } else { + mCharsetReloadState = eCharsetReloadInit; + } + + // Save a pointer to the currently-loading history entry. + // nsDocShell::EndPageLoad will clear mLSHE, but we may need this history + // entry further down in this method. + nsCOMPtr<nsISHEntry> loadingSHE = mLSHE; + mozilla::Unused << loadingSHE; // XXX: Not sure if we need this anymore + + // + // one of many safeguards that prevent death and destruction if + // someone is so very very rude as to bring this window down + // during this load handler. + // + nsCOMPtr<nsIDocShell> kungFuDeathGrip(this); + + // Notify the ContentViewer that the Document has finished loading. This + // will cause any OnLoad(...) and PopState(...) handlers to fire. + if (!mEODForCurrentDocument && mContentViewer) { + mIsExecutingOnLoadHandler = true; + nsCOMPtr<nsIContentViewer> contentViewer = mContentViewer; + contentViewer->LoadComplete(aStatus); + mIsExecutingOnLoadHandler = false; + + mEODForCurrentDocument = true; + } + /* Check if the httpChannel has any cache-control related response headers, + * like no-store, no-cache. If so, update SHEntry so that + * when a user goes back/forward to this page, we appropriately do + * form value restoration or load from server. + */ + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel)); + if (!httpChannel) { + // HttpChannel could be hiding underneath a Multipart channel. + GetHttpChannel(aChannel, getter_AddRefs(httpChannel)); + } + + if (httpChannel) { + // figure out if SH should be saving layout state. + bool discardLayoutState = ShouldDiscardLayoutState(httpChannel); + if (mLSHE && discardLayoutState && (mLoadType & LOAD_CMD_NORMAL) && + (mLoadType != LOAD_BYPASS_HISTORY) && (mLoadType != LOAD_ERROR_PAGE)) { + mLSHE->SetSaveLayoutStateFlag(false); + } + } + + // Clear mLSHE after calling the onLoadHandlers. This way, if the + // onLoadHandler tries to load something different in + // itself or one of its children, we can deal with it appropriately. + if (mLSHE) { + mLSHE->SetLoadType(LOAD_HISTORY); + + // Clear the mLSHE reference to indicate document loading is done one + // way or another. + SetHistoryEntryAndUpdateBC(Some(nullptr), Nothing()); + } + mActiveEntryIsLoadingFromSessionHistory = false; + + // if there's a refresh header in the channel, this method + // will set it up for us. + if (mBrowsingContext->IsActive() || !mDisableMetaRefreshWhenInactive) + RefreshURIFromQueue(); + + // Test whether this is the top frame or a subframe + bool isTopFrame = mBrowsingContext->IsTop(); + + bool hadErrorStatus = false; + // If status code indicates an error it means that DocumentChannel already + // tried to fixup the uri and failed. Throw an error dialog box here. + if (NS_FAILED(aStatus)) { + // If we got CONTENT_BLOCKED from EndPageLoad, then we need to fire + // the error event to our embedder, since tests are relying on this. + // The error event is usually fired by the caller of InternalLoad, but + // this particular error can happen asynchronously. + // Bug 1629201 is filed for having much clearer decision making around + // which cases need error events. + bool fireFrameErrorEvent = (aStatus == NS_ERROR_CONTENT_BLOCKED_SHOW_ALT || + aStatus == NS_ERROR_CONTENT_BLOCKED); + UnblockEmbedderLoadEventForFailure(fireFrameErrorEvent); + + bool isInitialDocument = + !GetExtantDocument() || GetExtantDocument()->IsInitialDocument(); + bool skippedUnknownProtocolNavigation = false; + aStatus = FilterStatusForErrorPage(aStatus, aChannel, mLoadType, isTopFrame, + mBrowsingContext->GetUseErrorPages(), + isInitialDocument, + &skippedUnknownProtocolNavigation); + hadErrorStatus = true; + if (NS_FAILED(aStatus)) { + if (!mIsBeingDestroyed) { + DisplayLoadError(aStatus, url, nullptr, aChannel); + } + } else if (skippedUnknownProtocolNavigation) { + nsTArray<nsString> params; + if (NS_FAILED( + NS_GetSanitizedURIStringFromURI(url, *params.AppendElement()))) { + params.LastElement().AssignLiteral(u"(unknown uri)"); + } + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "DOM"_ns, GetExtantDocument(), + nsContentUtils::eDOM_PROPERTIES, "UnknownProtocolNavigationPrevented", + params); + } + } else { + // If we have a host + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + PredictorLearnRedirect(url, aChannel, loadInfo->GetOriginAttributes()); + } + + if (hadErrorStatus) { + // Don't send session store updates if the reason EndPageLoad was called is + // because we are process switching. Sometimes the update takes too long and + // incorrectly overrides session store data from the following load. + return NS_OK; + } + if (StaticPrefs::browser_sessionstore_platform_collection_AtStartup()) { + if (WindowContext* windowContext = + mBrowsingContext->GetCurrentWindowContext()) { + using Change = SessionStoreChangeListener::Change; + + // We've finished loading the page and now we want to collect all the + // session store state that the page is initialized with. + SessionStoreChangeListener::CollectSessionStoreData( + windowContext, + EnumSet<Change>(Change::Input, Change::Scroll, Change::SessionHistory, + Change::WireFrame)); + } + } + + return NS_OK; +} + +//***************************************************************************** +// nsDocShell: Content Viewer Management +//***************************************************************************** + +nsresult nsDocShell::EnsureContentViewer() { + if (mContentViewer) { + return NS_OK; + } + if (mIsBeingDestroyed) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIContentSecurityPolicy> cspToInheritForAboutBlank; + nsCOMPtr<nsIURI> baseURI; + nsIPrincipal* principal = GetInheritedPrincipal(false); + nsIPrincipal* partitionedPrincipal = GetInheritedPrincipal(false, true); + + nsCOMPtr<nsIDocShellTreeItem> parentItem; + GetInProcessSameTypeParent(getter_AddRefs(parentItem)); + if (parentItem) { + if (nsCOMPtr<nsPIDOMWindowOuter> domWin = GetWindow()) { + nsCOMPtr<Element> parentElement = domWin->GetFrameElementInternal(); + if (parentElement) { + baseURI = parentElement->GetBaseURI(); + cspToInheritForAboutBlank = parentElement->GetCsp(); + } + } + } + + nsresult rv = CreateAboutBlankContentViewer( + principal, partitionedPrincipal, cspToInheritForAboutBlank, baseURI, + /* aIsInitialDocument */ true); + + NS_ENSURE_STATE(mContentViewer); + + if (NS_SUCCEEDED(rv)) { + RefPtr<Document> doc(GetDocument()); + MOZ_ASSERT(doc, + "Should have doc if CreateAboutBlankContentViewer " + "succeeded!"); + MOZ_ASSERT(doc->IsInitialDocument(), "Document should be initial document"); + + // Documents created using EnsureContentViewer may be transient + // placeholders created by framescripts before content has a + // chance to load. In some cases, window.open(..., "noopener") + // will create such a document and then synchronously tear it + // down, firing a "pagehide" event. Doing so violates our + // assertions about DocGroups. It's easier to silence the + // assertion here than to avoid creating the extra document. + doc->IgnoreDocGroupMismatches(); + } + + return rv; +} + +nsresult nsDocShell::CreateAboutBlankContentViewer( + nsIPrincipal* aPrincipal, nsIPrincipal* aPartitionedPrincipal, + nsIContentSecurityPolicy* aCSP, nsIURI* aBaseURI, bool aIsInitialDocument, + const Maybe<nsILoadInfo::CrossOriginEmbedderPolicy>& aCOEP, + bool aTryToSaveOldPresentation, bool aCheckPermitUnload, + WindowGlobalChild* aActor) { + RefPtr<Document> blankDoc; + nsCOMPtr<nsIContentViewer> viewer; + nsresult rv = NS_ERROR_FAILURE; + + MOZ_ASSERT_IF(aActor, aActor->DocumentPrincipal() == aPrincipal); + + /* mCreatingDocument should never be true at this point. However, it's + a theoretical possibility. We want to know about it and make it stop, + and this sounds like a job for an assertion. */ + NS_ASSERTION(!mCreatingDocument, + "infinite(?) loop creating document averted"); + if (mCreatingDocument) { + return NS_ERROR_FAILURE; + } + + if (!mBrowsingContext->AncestorsAreCurrent() || + mBrowsingContext->IsInBFCache()) { + mBrowsingContext->RemoveRootFromBFCacheSync(); + return NS_ERROR_NOT_AVAILABLE; + } + + // mContentViewer->PermitUnload may release |this| docshell. + nsCOMPtr<nsIDocShell> kungFuDeathGrip(this); + + AutoRestore<bool> creatingDocument(mCreatingDocument); + mCreatingDocument = true; + + if (aPrincipal && !aPrincipal->IsSystemPrincipal() && + mItemType != typeChrome) { + MOZ_ASSERT(aPrincipal->OriginAttributesRef() == + mBrowsingContext->OriginAttributesRef()); + } + + // Make sure timing is created. But first record whether we had it + // already, so we don't clobber the timing for an in-progress load. + bool hadTiming = mTiming; + bool toBeReset = MaybeInitTiming(); + if (mContentViewer) { + if (aCheckPermitUnload) { + // We've got a content viewer already. Make sure the user + // permits us to discard the current document and replace it + // with about:blank. And also ensure we fire the unload events + // in the current document. + + // Unload gets fired first for + // document loaded from the session history. + mTiming->NotifyBeforeUnload(); + + bool okToUnload; + rv = mContentViewer->PermitUnload(&okToUnload); + + if (NS_SUCCEEDED(rv) && !okToUnload) { + // The user chose not to unload the page, interrupt the load. + MaybeResetInitTiming(toBeReset); + return NS_ERROR_FAILURE; + } + if (mTiming) { + mTiming->NotifyUnloadAccepted(mCurrentURI); + } + } + + mSavingOldViewer = + aTryToSaveOldPresentation && + CanSavePresentation(LOAD_NORMAL, nullptr, nullptr, + /* aReportBFCacheComboTelemetry */ true); + + // Make sure to blow away our mLoadingURI just in case. No loads + // from inside this pagehide. + mLoadingURI = nullptr; + + // Stop any in-progress loading, so that we don't accidentally trigger any + // PageShow notifications from Embed() interrupting our loading below. + Stop(); + + // Notify the current document that it is about to be unloaded!! + // + // It is important to fire the unload() notification *before* any state + // is changed within the DocShell - otherwise, javascript will get the + // wrong information :-( + // + (void)FirePageHideNotification(!mSavingOldViewer); + // pagehide notification might destroy this docshell. + if (mIsBeingDestroyed) { + return NS_ERROR_DOCSHELL_DYING; + } + } + + // Now make sure we don't think we're in the middle of firing unload after + // this point. This will make us fire unload when the about:blank document + // unloads... but that's ok, more or less. Would be nice if it fired load + // too, of course. + mFiredUnloadEvent = false; + + nsCOMPtr<nsIDocumentLoaderFactory> docFactory = + nsContentUtils::FindInternalContentViewer("text/html"_ns); + + if (docFactory) { + nsCOMPtr<nsIPrincipal> principal, partitionedPrincipal; + const uint32_t sandboxFlags = + mBrowsingContext->GetHasLoadedNonInitialDocument() + ? mBrowsingContext->GetSandboxFlags() + : mBrowsingContext->GetInitialSandboxFlags(); + // If we're sandboxed, then create a new null principal. We skip + // this if we're being created from WindowGlobalChild, since in + // that case we already have a null principal if required. + // We can't compare againt the BrowsingContext sandbox flag, since + // the value was taken when the load initiated and may have since + // changed. + if ((sandboxFlags & SANDBOXED_ORIGIN) && !aActor) { + if (aPrincipal) { + principal = NullPrincipal::CreateWithInheritedAttributes(aPrincipal); + } else { + principal = NullPrincipal::Create(GetOriginAttributes()); + } + partitionedPrincipal = principal; + } else { + principal = aPrincipal; + partitionedPrincipal = aPartitionedPrincipal; + } + + // We cannot get the foreign partitioned prinicpal for the initial + // about:blank page. So, we change to check if we need to use the + // partitioned principal for the service worker here. + MaybeCreateInitialClientSource( + StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker( + this) + ? partitionedPrincipal + : principal); + + // generate (about:blank) document to load + blankDoc = nsContentDLF::CreateBlankDocument(mLoadGroup, principal, + partitionedPrincipal, this); + if (blankDoc) { + // Hack: manually set the CSP for the new document + // Please create an actual copy of the CSP (do not share the same + // reference) otherwise appending a new policy within the new + // document will be incorrectly propagated to the opening doc. + if (aCSP) { + RefPtr<nsCSPContext> cspToInherit = new nsCSPContext(); + cspToInherit->InitFromOther(static_cast<nsCSPContext*>(aCSP)); + blankDoc->SetCsp(cspToInherit); + } + + blankDoc->SetIsInitialDocument(aIsInitialDocument); + + blankDoc->SetEmbedderPolicy(aCOEP); + + // Hack: set the base URI manually, since this document never + // got Reset() with a channel. + blankDoc->SetBaseURI(aBaseURI); + + // Copy our sandbox flags to the document. These are immutable + // after being set here. + blankDoc->SetSandboxFlags(sandboxFlags); + + blankDoc->InitFeaturePolicy(); + + // create a content viewer for us and the new document + docFactory->CreateInstanceForDocument( + NS_ISUPPORTS_CAST(nsIDocShell*, this), blankDoc, "view", + getter_AddRefs(viewer)); + + // hook 'em up + if (viewer) { + viewer->SetContainer(this); + rv = Embed(viewer, aActor, true, false, nullptr, mCurrentURI); + NS_ENSURE_SUCCESS(rv, rv); + + SetCurrentURI(blankDoc->GetDocumentURI(), nullptr, + /* aFireLocationChange */ true, + /* aIsInitialAboutBlank */ true, + /* aLocationFlags */ 0); + rv = mIsBeingDestroyed ? NS_ERROR_NOT_AVAILABLE : NS_OK; + } + } + } + + // The transient about:blank viewer doesn't have a session history entry. + SetHistoryEntryAndUpdateBC(Nothing(), Some(nullptr)); + + // Clear out our mTiming like we would in EndPageLoad, if we didn't + // have one before entering this function. + if (!hadTiming) { + mTiming = nullptr; + mBlankTiming = true; + } + + return rv; +} + +NS_IMETHODIMP +nsDocShell::CreateAboutBlankContentViewer(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal, + nsIContentSecurityPolicy* aCSP) { + return CreateAboutBlankContentViewer(aPrincipal, aPartitionedPrincipal, aCSP, + nullptr, /* aIsInitialDocument */ false); +} + +nsresult nsDocShell::CreateContentViewerForActor( + WindowGlobalChild* aWindowActor) { + MOZ_ASSERT(aWindowActor); + + // FIXME: WindowGlobalChild should provide the PartitionedPrincipal. + // FIXME: We may want to support non-initial documents here. + nsresult rv = CreateAboutBlankContentViewer( + aWindowActor->DocumentPrincipal(), aWindowActor->DocumentPrincipal(), + /* aCsp */ nullptr, + /* aBaseURI */ nullptr, + /* aIsInitialDocument */ true, + /* aCOEP */ Nothing(), + /* aTryToSaveOldPresentation */ true, + /* aCheckPermitUnload */ true, aWindowActor); +#ifdef DEBUG + if (NS_SUCCEEDED(rv)) { + RefPtr<Document> doc(GetDocument()); + MOZ_ASSERT( + doc, + "Should have a document if CreateAboutBlankContentViewer succeeded"); + MOZ_ASSERT(doc->GetOwnerGlobal() == aWindowActor->GetWindowGlobal(), + "New document should be in the same global as our actor"); + MOZ_ASSERT(doc->IsInitialDocument(), + "New document should be an initial document"); + } +#endif + + return rv; +} + +bool nsDocShell::CanSavePresentation(uint32_t aLoadType, + nsIRequest* aNewRequest, + Document* aNewDocument, + bool aReportBFCacheComboTelemetry) { + if (!mOSHE) { + return false; // no entry to save into + } + + MOZ_ASSERT(!mozilla::SessionHistoryInParent(), + "mOSHE cannot be non-null with SHIP"); + nsCOMPtr<nsIContentViewer> viewer = mOSHE->GetContentViewer(); + if (viewer) { + NS_WARNING("mOSHE already has a content viewer!"); + return false; + } + + // Only save presentation for "normal" loads and link loads. Anything else + // probably wants to refetch the page, so caching the old presentation + // would be incorrect. + if (aLoadType != LOAD_NORMAL && aLoadType != LOAD_HISTORY && + aLoadType != LOAD_LINK && aLoadType != LOAD_STOP_CONTENT && + aLoadType != LOAD_STOP_CONTENT_AND_REPLACE && + aLoadType != LOAD_ERROR_PAGE) { + return false; + } + + // If the session history entry has the saveLayoutState flag set to false, + // then we should not cache the presentation. + if (!mOSHE->GetSaveLayoutStateFlag()) { + return false; + } + + // If the document is not done loading, don't cache it. + if (!mScriptGlobal || mScriptGlobal->IsLoading()) { + MOZ_LOG(gPageCacheLog, mozilla::LogLevel::Verbose, + ("Blocked due to document still loading")); + return false; + } + + if (mScriptGlobal->WouldReuseInnerWindow(aNewDocument)) { + return false; + } + + // Avoid doing the work of saving the presentation state in the case where + // the content viewer cache is disabled. + if (nsSHistory::GetMaxTotalViewers() == 0) { + return false; + } + + // Don't cache the content viewer if we're in a subframe. + if (mBrowsingContext->GetParent()) { + return false; // this is a subframe load + } + + // If the document does not want its presentation cached, then don't. + RefPtr<Document> doc = mScriptGlobal->GetExtantDoc(); + + uint32_t bfCacheCombo = 0; + bool canSavePresentation = + doc->CanSavePresentation(aNewRequest, bfCacheCombo, true); + MOZ_ASSERT_IF(canSavePresentation, bfCacheCombo == 0); + if (canSavePresentation && doc->IsTopLevelContentDocument()) { + auto* browsingContextGroup = mBrowsingContext->Group(); + nsTArray<RefPtr<BrowsingContext>>& topLevelContext = + browsingContextGroup->Toplevels(); + + for (const auto& browsingContext : topLevelContext) { + if (browsingContext != mBrowsingContext) { + if (StaticPrefs::docshell_shistory_bfcache_require_no_opener()) { + canSavePresentation = false; + } + bfCacheCombo |= BFCacheStatus::NOT_ONLY_TOPLEVEL_IN_BCG; + break; + } + } + } + + if (aReportBFCacheComboTelemetry) { + ReportBFCacheComboTelemetry(bfCacheCombo); + } + return doc && canSavePresentation; +} + +/* static */ +void nsDocShell::ReportBFCacheComboTelemetry(uint32_t aCombo) { + // There are 11 possible reasons to make a request fails to use BFCache + // (see BFCacheStatus in dom/base/Document.h), and we'd like to record + // the common combinations for reasons which make requests fail to use + // BFCache. These combinations are generated based on some local browsings, + // we need to adjust them when necessary. + enum BFCacheStatusCombo : uint32_t { + BFCACHE_SUCCESS, + NOT_ONLY_TOPLEVEL = mozilla::dom::BFCacheStatus::NOT_ONLY_TOPLEVEL_IN_BCG, + // If both unload and beforeunload listeners are presented, it'll be + // recorded as unload + UNLOAD = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER, + UNLOAD_REQUEST = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER | + mozilla::dom::BFCacheStatus::REQUEST, + REQUEST = mozilla::dom::BFCacheStatus::REQUEST, + UNLOAD_REQUEST_PEER = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER | + mozilla::dom::BFCacheStatus::REQUEST | + mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION, + UNLOAD_REQUEST_PEER_MSE = + mozilla::dom::BFCacheStatus::UNLOAD_LISTENER | + mozilla::dom::BFCacheStatus::REQUEST | + mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION | + mozilla::dom::BFCacheStatus::CONTAINS_MSE_CONTENT, + UNLOAD_REQUEST_MSE = mozilla::dom::BFCacheStatus::UNLOAD_LISTENER | + mozilla::dom::BFCacheStatus::REQUEST | + mozilla::dom::BFCacheStatus::CONTAINS_MSE_CONTENT, + SUSPENDED_UNLOAD_REQUEST_PEER = + mozilla::dom::BFCacheStatus::SUSPENDED | + mozilla::dom::BFCacheStatus::UNLOAD_LISTENER | + mozilla::dom::BFCacheStatus::REQUEST | + mozilla::dom::BFCacheStatus::ACTIVE_PEER_CONNECTION, + REMOTE_SUBFRAMES = mozilla::dom::BFCacheStatus::CONTAINS_REMOTE_SUBFRAMES, + BEFOREUNLOAD = mozilla::dom::BFCacheStatus::BEFOREUNLOAD_LISTENER, + }; + + // Beforeunload is recorded as a blocker only if it is the only one to block + // bfcache. + if (aCombo != mozilla::dom::BFCacheStatus::BEFOREUNLOAD_LISTENER) { + aCombo &= ~mozilla::dom::BFCacheStatus::BEFOREUNLOAD_LISTENER; + } + switch (aCombo) { + case BFCACHE_SUCCESS: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::BFCache_Success); + break; + case NOT_ONLY_TOPLEVEL: + if (StaticPrefs::docshell_shistory_bfcache_require_no_opener()) { + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::Other); + break; + } + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::BFCache_Success); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::Success_Not_Toplevel); + break; + case UNLOAD: + Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Unload); + break; + case BEFOREUNLOAD: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::Beforeunload); + break; + case UNLOAD_REQUEST: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::Unload_Req); + break; + case REQUEST: + Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Req); + break; + case UNLOAD_REQUEST_PEER: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_Peer); + break; + case UNLOAD_REQUEST_PEER_MSE: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_Peer_MSE); + break; + case UNLOAD_REQUEST_MSE: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::Unload_Req_MSE); + break; + case SUSPENDED_UNLOAD_REQUEST_PEER: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::SPD_Unload_Req_Peer); + break; + case REMOTE_SUBFRAMES: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_BFCACHE_COMBO::Remote_Subframes); + break; + default: + Telemetry::AccumulateCategorical(Telemetry::LABELS_BFCACHE_COMBO::Other); + break; + } +}; + +void nsDocShell::ReattachEditorToWindow(nsISHEntry* aSHEntry) { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + MOZ_ASSERT(!mIsBeingDestroyed); + + NS_ASSERTION(!mEditorData, + "Why reattach an editor when we already have one?"); + NS_ASSERTION(aSHEntry && aSHEntry->HasDetachedEditor(), + "Reattaching when there's not a detached editor."); + + if (mEditorData || !aSHEntry) { + return; + } + + mEditorData = WrapUnique(aSHEntry->ForgetEditorData()); + if (mEditorData) { +#ifdef DEBUG + nsresult rv = +#endif + mEditorData->ReattachToWindow(this); + NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to reattach editing session"); + } +} + +void nsDocShell::DetachEditorFromWindow() { + if (!mEditorData || mEditorData->WaitingForLoad()) { + // If there's nothing to detach, or if the editor data is actually set + // up for the _new_ page that's coming in, don't detach. + return; + } + + NS_ASSERTION(!mOSHE || !mOSHE->HasDetachedEditor(), + "Detaching editor when it's already detached."); + + nsresult res = mEditorData->DetachFromWindow(); + NS_ASSERTION(NS_SUCCEEDED(res), "Failed to detach editor"); + + if (NS_SUCCEEDED(res)) { + // Make mOSHE hold the owning ref to the editor data. + if (mOSHE) { + MOZ_ASSERT(!mIsBeingDestroyed || !mOSHE->HasDetachedEditor(), + "We should not set the editor data again once after we " + "detached the editor data during destroying this docshell"); + mOSHE->SetEditorData(mEditorData.release()); + } else { + mEditorData = nullptr; + } + } + +#ifdef DEBUG + { + bool isEditable; + GetEditable(&isEditable); + NS_ASSERTION(!isEditable, + "Window is still editable after detaching editor."); + } +#endif // DEBUG +} + +nsresult nsDocShell::CaptureState() { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + + if (!mOSHE || mOSHE == mLSHE) { + // No entry to save into, or we're replacing the existing entry. + return NS_ERROR_FAILURE; + } + + if (!mScriptGlobal) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISupports> windowState = mScriptGlobal->SaveWindowState(); + NS_ENSURE_TRUE(windowState, NS_ERROR_FAILURE); + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gPageCacheLog, LogLevel::Debug))) { + nsAutoCString spec; + nsCOMPtr<nsIURI> uri = mOSHE->GetURI(); + if (uri) { + uri->GetSpec(spec); + } + MOZ_LOG(gPageCacheLog, LogLevel::Debug, + ("Saving presentation into session history, URI: %s", spec.get())); + } + + mOSHE->SetWindowState(windowState); + + // Suspend refresh URIs and save off the timer queue + mOSHE->SetRefreshURIList(mSavedRefreshURIList); + + // Capture the current content viewer bounds. + if (mContentViewer) { + nsIntRect bounds; + mContentViewer->GetBounds(bounds); + mOSHE->SetViewerBounds(bounds); + } + + // Capture the docshell hierarchy. + mOSHE->ClearChildShells(); + + uint32_t childCount = mChildList.Length(); + for (uint32_t i = 0; i < childCount; ++i) { + nsCOMPtr<nsIDocShellTreeItem> childShell = do_QueryInterface(ChildAt(i)); + NS_ASSERTION(childShell, "null child shell"); + + mOSHE->AddChildShell(childShell); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::RestorePresentationEvent::Run() { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + + if (mDocShell && NS_FAILED(mDocShell->RestoreFromHistory())) { + NS_WARNING("RestoreFromHistory failed"); + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::BeginRestore(nsIContentViewer* aContentViewer, bool aTop) { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + + nsresult rv; + if (!aContentViewer) { + rv = EnsureContentViewer(); + NS_ENSURE_SUCCESS(rv, rv); + + aContentViewer = mContentViewer; + } + + // Dispatch events for restoring the presentation. We try to simulate + // the progress notifications loading the document would cause, so we add + // the document's channel to the loadgroup to initiate stateChange + // notifications. + + RefPtr<Document> doc = aContentViewer->GetDocument(); + if (doc) { + nsIChannel* channel = doc->GetChannel(); + if (channel) { + mEODForCurrentDocument = false; + mIsRestoringDocument = true; + mLoadGroup->AddRequest(channel, nullptr); + mIsRestoringDocument = false; + } + } + + if (!aTop) { + // This point corresponds to us having gotten OnStartRequest or + // STATE_START, so do the same thing that CreateContentViewer does at + // this point to ensure that unload/pagehide events for this document + // will fire when it's unloaded again. + mFiredUnloadEvent = false; + + // For non-top frames, there is no notion of making sure that the + // previous document is in the domwindow when STATE_START notifications + // happen. We can just call BeginRestore for all of the child shells + // now. + rv = BeginRestoreChildren(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult nsDocShell::BeginRestoreChildren() { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + + for (auto* childDocLoader : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShell> child = do_QueryObject(childDocLoader); + if (child) { + nsresult rv = child->BeginRestore(nullptr, false); + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::FinishRestore() { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + + // First we call finishRestore() on our children. In the simulated load, + // all of the child frames finish loading before the main document. + + for (auto* childDocLoader : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShell> child = do_QueryObject(childDocLoader); + if (child) { + child->FinishRestore(); + } + } + + if (mOSHE && mOSHE->HasDetachedEditor()) { + ReattachEditorToWindow(mOSHE); + } + + RefPtr<Document> doc = GetDocument(); + if (doc) { + // Finally, we remove the request from the loadgroup. This will + // cause onStateChange(STATE_STOP) to fire, which will fire the + // pageshow event to the chrome. + + nsIChannel* channel = doc->GetChannel(); + if (channel) { + mIsRestoringDocument = true; + mLoadGroup->RemoveRequest(channel, nullptr, NS_OK); + mIsRestoringDocument = false; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetRestoringDocument(bool* aRestoring) { + *aRestoring = mIsRestoringDocument; + return NS_OK; +} + +nsresult nsDocShell::RestorePresentation(nsISHEntry* aSHEntry, + bool* aRestoring) { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + MOZ_ASSERT(!mIsBeingDestroyed); + + NS_ASSERTION(mLoadType & LOAD_CMD_HISTORY, + "RestorePresentation should only be called for history loads"); + + nsCOMPtr<nsIContentViewer> viewer = aSHEntry->GetContentViewer(); + + nsAutoCString spec; + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gPageCacheLog, LogLevel::Debug))) { + nsCOMPtr<nsIURI> uri = aSHEntry->GetURI(); + if (uri) { + uri->GetSpec(spec); + } + } + + *aRestoring = false; + + if (!viewer) { + MOZ_LOG(gPageCacheLog, LogLevel::Debug, + ("no saved presentation for uri: %s", spec.get())); + return NS_OK; + } + + // We need to make sure the content viewer's container is this docshell. + // In subframe navigation, it's possible for the docshell that the + // content viewer was originally loaded into to be replaced with a + // different one. We don't currently support restoring the presentation + // in that case. + + nsCOMPtr<nsIDocShell> container; + viewer->GetContainer(getter_AddRefs(container)); + if (!::SameCOMIdentity(container, GetAsSupports(this))) { + MOZ_LOG(gPageCacheLog, LogLevel::Debug, + ("No valid container, clearing presentation")); + aSHEntry->SetContentViewer(nullptr); + return NS_ERROR_FAILURE; + } + + NS_ASSERTION(mContentViewer != viewer, "Restoring existing presentation"); + + MOZ_LOG(gPageCacheLog, LogLevel::Debug, + ("restoring presentation from session history: %s", spec.get())); + + SetHistoryEntryAndUpdateBC(Some(aSHEntry), Nothing()); + + // Post an event that will remove the request after we've returned + // to the event loop. This mimics the way it is called by nsIChannel + // implementations. + + // Revoke any pending restore (just in case). + NS_ASSERTION(!mRestorePresentationEvent.IsPending(), + "should only have one RestorePresentationEvent"); + mRestorePresentationEvent.Revoke(); + + RefPtr<RestorePresentationEvent> evt = new RestorePresentationEvent(this); + nsresult rv = Dispatch(TaskCategory::Other, do_AddRef(evt)); + if (NS_SUCCEEDED(rv)) { + mRestorePresentationEvent = evt.get(); + // The rest of the restore processing will happen on our event + // callback. + *aRestoring = true; + } + + return rv; +} + +namespace { +class MOZ_STACK_CLASS PresentationEventForgetter { + public: + explicit PresentationEventForgetter( + nsRevocableEventPtr<nsDocShell::RestorePresentationEvent>& + aRestorePresentationEvent) + : mRestorePresentationEvent(aRestorePresentationEvent), + mEvent(aRestorePresentationEvent.get()) {} + + ~PresentationEventForgetter() { Forget(); } + + void Forget() { + if (mRestorePresentationEvent.get() == mEvent) { + mRestorePresentationEvent.Forget(); + mEvent = nullptr; + } + } + + private: + nsRevocableEventPtr<nsDocShell::RestorePresentationEvent>& + mRestorePresentationEvent; + RefPtr<nsDocShell::RestorePresentationEvent> mEvent; +}; + +} // namespace + +bool nsDocShell::SandboxFlagsImplyCookies(const uint32_t& aSandboxFlags) { + return (aSandboxFlags & (SANDBOXED_ORIGIN | SANDBOXED_SCRIPTS)) == 0; +} + +nsresult nsDocShell::RestoreFromHistory() { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + MOZ_ASSERT(mRestorePresentationEvent.IsPending()); + PresentationEventForgetter forgetter(mRestorePresentationEvent); + + // This section of code follows the same ordering as CreateContentViewer. + if (!mLSHE) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIContentViewer> viewer = mLSHE->GetContentViewer(); + if (!viewer) { + return NS_ERROR_FAILURE; + } + + if (mSavingOldViewer) { + // We determined that it was safe to cache the document presentation + // at the time we initiated the new load. We need to check whether + // it's still safe to do so, since there may have been DOM mutations + // or new requests initiated. + RefPtr<Document> doc = viewer->GetDocument(); + nsIRequest* request = nullptr; + if (doc) { + request = doc->GetChannel(); + } + mSavingOldViewer = CanSavePresentation( + mLoadType, request, doc, /* aReportBFCacheComboTelemetry */ false); + } + + // Protect against mLSHE going away via a load triggered from + // pagehide or unload. + nsCOMPtr<nsISHEntry> origLSHE = mLSHE; + + // Make sure to blow away our mLoadingURI just in case. No loads + // from inside this pagehide. + mLoadingURI = nullptr; + + // Notify the old content viewer that it's being hidden. + FirePageHideNotification(!mSavingOldViewer); + // pagehide notification might destroy this docshell. + if (mIsBeingDestroyed) { + return NS_ERROR_DOCSHELL_DYING; + } + + // If mLSHE was changed as a result of the pagehide event, then + // something else was loaded. Don't finish restoring. + if (mLSHE != origLSHE) { + return NS_OK; + } + + // Add the request to our load group. We do this before swapping out + // the content viewers so that consumers of STATE_START can access + // the old document. We only deal with the toplevel load at this time -- + // to be consistent with normal document loading, subframes cannot start + // loading until after data arrives, which is after STATE_START completes. + + RefPtr<RestorePresentationEvent> currentPresentationRestoration = + mRestorePresentationEvent.get(); + Stop(); + // Make sure we're still restoring the same presentation. + // If we aren't, docshell is in process doing another load already. + NS_ENSURE_STATE(currentPresentationRestoration == + mRestorePresentationEvent.get()); + BeginRestore(viewer, true); + NS_ENSURE_STATE(currentPresentationRestoration == + mRestorePresentationEvent.get()); + forgetter.Forget(); + + // Set mFiredUnloadEvent = false so that the unload handler for the + // *new* document will fire. + mFiredUnloadEvent = false; + + mURIResultedInDocument = true; + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (rootSH) { + mPreviousEntryIndex = rootSH->Index(); + rootSH->LegacySHistory()->UpdateIndex(); + mLoadedEntryIndex = rootSH->Index(); + MOZ_LOG(gPageCacheLog, LogLevel::Verbose, + ("Previous index: %d, Loaded index: %d", mPreviousEntryIndex, + mLoadedEntryIndex)); + } + + // Rather than call Embed(), we will retrieve the viewer from the session + // history entry and swap it in. + // XXX can we refactor this so that we can just call Embed()? + PersistLayoutHistoryState(); + nsresult rv; + if (mContentViewer) { + if (mSavingOldViewer && NS_FAILED(CaptureState())) { + if (mOSHE) { + mOSHE->SyncPresentationState(); + } + mSavingOldViewer = false; + } + } + + mSavedRefreshURIList = nullptr; + + // In cases where we use a transient about:blank viewer between loads, + // we never show the transient viewer, so _its_ previous viewer is never + // unhooked from the view hierarchy. Destroy any such previous viewer now, + // before we grab the root view sibling, so that we don't grab a view + // that's about to go away. + + if (mContentViewer) { + // Make sure to hold a strong ref to previousViewer here while we + // drop the reference to it from mContentViewer. + nsCOMPtr<nsIContentViewer> previousViewer = + mContentViewer->GetPreviousViewer(); + if (previousViewer) { + mContentViewer->SetPreviousViewer(nullptr); + previousViewer->Destroy(); + } + } + + // Save off the root view's parent and sibling so that we can insert the + // new content viewer's root view at the same position. Also save the + // bounds of the root view's widget. + + nsView* rootViewSibling = nullptr; + nsView* rootViewParent = nullptr; + nsIntRect newBounds(0, 0, 0, 0); + + PresShell* oldPresShell = GetPresShell(); + if (oldPresShell) { + nsViewManager* vm = oldPresShell->GetViewManager(); + if (vm) { + nsView* oldRootView = vm->GetRootView(); + + if (oldRootView) { + rootViewSibling = oldRootView->GetNextSibling(); + rootViewParent = oldRootView->GetParent(); + + mContentViewer->GetBounds(newBounds); + } + } + } + + nsCOMPtr<nsIContent> container; + RefPtr<Document> sibling; + if (rootViewParent && rootViewParent->GetParent()) { + nsIFrame* frame = rootViewParent->GetParent()->GetFrame(); + container = frame ? frame->GetContent() : nullptr; + } + if (rootViewSibling) { + nsIFrame* frame = rootViewSibling->GetFrame(); + sibling = frame ? frame->PresShell()->GetDocument() : nullptr; + } + + // Transfer ownership to mContentViewer. By ensuring that either the + // docshell or the session history, but not both, have references to the + // content viewer, we prevent the viewer from being torn down after + // Destroy() is called. + + if (mContentViewer) { + mContentViewer->Close(mSavingOldViewer ? mOSHE.get() : nullptr); + viewer->SetPreviousViewer(mContentViewer); + } + if (mOSHE && (!mContentViewer || !mSavingOldViewer)) { + // We don't plan to save a viewer in mOSHE; tell it to drop + // any other state it's holding. + mOSHE->SyncPresentationState(); + } + + // Order the mContentViewer setup just like Embed does. + mContentViewer = nullptr; + + // Now that we're about to switch documents, forget all of our children. + // Note that we cached them as needed up in CaptureState above. + DestroyChildren(); + + mContentViewer.swap(viewer); + + // Grab all of the related presentation from the SHEntry now. + // Clearing the viewer from the SHEntry will clear all of this state. + nsCOMPtr<nsISupports> windowState = mLSHE->GetWindowState(); + mLSHE->SetWindowState(nullptr); + + bool sticky = mLSHE->GetSticky(); + + RefPtr<Document> document = mContentViewer->GetDocument(); + + nsCOMArray<nsIDocShellTreeItem> childShells; + int32_t i = 0; + nsCOMPtr<nsIDocShellTreeItem> child; + while (NS_SUCCEEDED(mLSHE->ChildShellAt(i++, getter_AddRefs(child))) && + child) { + childShells.AppendObject(child); + } + + // get the previous content viewer size + nsIntRect oldBounds(0, 0, 0, 0); + mLSHE->GetViewerBounds(oldBounds); + + // Restore the refresh URI list. The refresh timers will be restarted + // when EndPageLoad() is called. + nsCOMPtr<nsIMutableArray> refreshURIList = mLSHE->GetRefreshURIList(); + + // Reattach to the window object. + mIsRestoringDocument = true; // for MediaDocument::BecomeInteractive + rv = mContentViewer->Open(windowState, mLSHE); + mIsRestoringDocument = false; + + // Hack to keep nsDocShellEditorData alive across the + // SetContentViewer(nullptr) call below. + UniquePtr<nsDocShellEditorData> data(mLSHE->ForgetEditorData()); + + // Now remove it from the cached presentation. + mLSHE->SetContentViewer(nullptr); + mEODForCurrentDocument = false; + + mLSHE->SetEditorData(data.release()); + +#ifdef DEBUG + { + nsCOMPtr<nsIMutableArray> refreshURIs = mLSHE->GetRefreshURIList(); + nsCOMPtr<nsIDocShellTreeItem> childShell; + mLSHE->ChildShellAt(0, getter_AddRefs(childShell)); + NS_ASSERTION(!refreshURIs && !childShell, + "SHEntry should have cleared presentation state"); + } +#endif + + // Restore the sticky state of the viewer. The viewer has set this state + // on the history entry in Destroy() just before marking itself non-sticky, + // to avoid teardown of the presentation. + mContentViewer->SetSticky(sticky); + + NS_ENSURE_SUCCESS(rv, rv); + + // mLSHE is now our currently-loaded document. + SetHistoryEntryAndUpdateBC(Nothing(), Some<nsISHEntry*>(mLSHE)); + + // We aren't going to restore any items from the LayoutHistoryState, + // but we don't want them to stay around in case the page is reloaded. + SetLayoutHistoryState(nullptr); + + // This is the end of our Embed() replacement + + mSavingOldViewer = false; + mEODForCurrentDocument = false; + + if (document) { + RefPtr<nsDocShell> parent = GetInProcessParentDocshell(); + if (parent) { + RefPtr<Document> d = parent->GetDocument(); + if (d) { + if (d->EventHandlingSuppressed()) { + document->SuppressEventHandling(d->EventHandlingSuppressed()); + } + } + } + + // Use the uri from the mLSHE we had when we entered this function + // (which need not match the document's URI if anchors are involved), + // since that's the history entry we're loading. Note that if we use + // origLSHE we don't have to worry about whether the entry in question + // is still mLSHE or whether it's now mOSHE. + nsCOMPtr<nsIURI> uri = origLSHE->GetURI(); + SetCurrentURI(uri, document->GetChannel(), /* aFireLocationChange */ true, + /* aIsInitialAboutBlank */ false, + /* aLocationFlags */ 0); + } + + // This is the end of our CreateContentViewer() replacement. + // Now we simulate a load. First, we restore the state of the javascript + // window object. + nsCOMPtr<nsPIDOMWindowOuter> privWin = GetWindow(); + NS_ASSERTION(privWin, "could not get nsPIDOMWindow interface"); + + // Now, dispatch a title change event which would happen as the + // <head> is parsed. + document->NotifyPossibleTitleChange(false); + + // Now we simulate appending child docshells for subframes. + for (i = 0; i < childShells.Count(); ++i) { + nsIDocShellTreeItem* childItem = childShells.ObjectAt(i); + nsCOMPtr<nsIDocShell> childShell = do_QueryInterface(childItem); + + // Make sure to not clobber the state of the child. Since AddChild + // always clobbers it, save it off first. + bool allowRedirects; + childShell->GetAllowMetaRedirects(&allowRedirects); + + bool allowSubframes; + childShell->GetAllowSubframes(&allowSubframes); + + bool allowImages; + childShell->GetAllowImages(&allowImages); + + bool allowMedia = childShell->GetAllowMedia(); + + bool allowDNSPrefetch; + childShell->GetAllowDNSPrefetch(&allowDNSPrefetch); + + bool allowContentRetargeting = childShell->GetAllowContentRetargeting(); + bool allowContentRetargetingOnChildren = + childShell->GetAllowContentRetargetingOnChildren(); + + // this.AddChild(child) calls child.SetDocLoaderParent(this), meaning that + // the child inherits our state. Among other things, this means that the + // child inherits our mPrivateBrowsingId, which is what we want. + AddChild(childItem); + + childShell->SetAllowMetaRedirects(allowRedirects); + childShell->SetAllowSubframes(allowSubframes); + childShell->SetAllowImages(allowImages); + childShell->SetAllowMedia(allowMedia); + childShell->SetAllowDNSPrefetch(allowDNSPrefetch); + childShell->SetAllowContentRetargeting(allowContentRetargeting); + childShell->SetAllowContentRetargetingOnChildren( + allowContentRetargetingOnChildren); + + rv = childShell->BeginRestore(nullptr, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Make sure to restore the window state after adding the child shells back + // to the tree. This is necessary for Thaw() and Resume() to propagate + // properly. + rv = privWin->RestoreWindowState(windowState); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<PresShell> presShell = GetPresShell(); + + // We may be displayed on a different monitor (or in a different + // HiDPI mode) than when we got into the history list. So we need + // to check if this has happened. See bug 838239. + + // Because the prescontext normally handles resolution changes via + // a runnable (see nsPresContext::UIResolutionChanged), its device + // context won't be -immediately- updated as a result of calling + // presShell->BackingScaleFactorChanged(). + + // But we depend on that device context when adjusting the view size + // via mContentViewer->SetBounds(newBounds) below. So we need to + // explicitly tell it to check for changed resolution here. + if (presShell) { + RefPtr<nsPresContext> pc = presShell->GetPresContext(); + if (pc->DeviceContext()->CheckDPIChange()) { + presShell->BackingScaleFactorChanged(); + } + // Recompute zoom and text-zoom and such. + pc->RecomputeBrowsingContextDependentData(); + } + + nsViewManager* newVM = presShell ? presShell->GetViewManager() : nullptr; + nsView* newRootView = newVM ? newVM->GetRootView() : nullptr; + + // Insert the new root view at the correct location in the view tree. + if (container) { + nsSubDocumentFrame* subDocFrame = + do_QueryFrame(container->GetPrimaryFrame()); + rootViewParent = subDocFrame ? subDocFrame->EnsureInnerView() : nullptr; + } else { + rootViewParent = nullptr; + } + if (sibling && sibling->GetPresShell() && + sibling->GetPresShell()->GetViewManager()) { + rootViewSibling = sibling->GetPresShell()->GetViewManager()->GetRootView(); + } else { + rootViewSibling = nullptr; + } + if (rootViewParent && newRootView && + newRootView->GetParent() != rootViewParent) { + nsViewManager* parentVM = rootViewParent->GetViewManager(); + if (parentVM) { + // InsertChild(parent, child, sib, true) inserts the child after + // sib in content order, which is before sib in view order. BUT + // when sib is null it inserts at the end of the the document + // order, i.e., first in view order. But when oldRootSibling is + // null, the old root as at the end of the view list --- last in + // content order --- and we want to call InsertChild(parent, child, + // nullptr, false) in that case. + parentVM->InsertChild(rootViewParent, newRootView, rootViewSibling, + rootViewSibling ? true : false); + + NS_ASSERTION(newRootView->GetNextSibling() == rootViewSibling, + "error in InsertChild"); + } + } + + nsCOMPtr<nsPIDOMWindowInner> privWinInner = privWin->GetCurrentInnerWindow(); + + // If parent is suspended, increase suspension count. + // This can't be done as early as event suppression since this + // depends on docshell tree. + privWinInner->SyncStateFromParentWindow(); + + // Now that all of the child docshells have been put into place, we can + // restart the timers for the window and all of the child frames. + privWinInner->Resume(); + + // Now that we have found the inner window of the page restored + // from the history, we have to make sure that + // performance.navigation.type is 2. + Performance* performance = privWinInner->GetPerformance(); + if (performance) { + performance->GetDOMTiming()->NotifyRestoreStart(); + } + + // Restore the refresh URI list. The refresh timers will be restarted + // when EndPageLoad() is called. + mRefreshURIList = refreshURIList; + + // Meta-refresh timers have been restarted for this shell, but not + // for our children. Walk the child shells and restart their timers. + for (auto* childDocLoader : mChildList.ForwardRange()) { + nsCOMPtr<nsIDocShell> child = do_QueryObject(childDocLoader); + if (child) { + child->ResumeRefreshURIs(); + } + } + + // Make sure this presentation is the same size as the previous + // presentation. If this is not the same size we showed it at last time, + // then we need to resize the widget. + + // XXXbryner This interacts poorly with Firefox's infobar. If the old + // presentation had the infobar visible, then we will resize the new + // presentation to that smaller size. However, firing the locationchanged + // event will hide the infobar, which will immediately resize the window + // back to the larger size. A future optimization might be to restore + // the presentation at the "wrong" size, then fire the locationchanged + // event and check whether the docshell's new size is the same as the + // cached viewer size (skipping the resize if they are equal). + + if (newRootView) { + if (!newBounds.IsEmpty() && !newBounds.IsEqualEdges(oldBounds)) { + MOZ_LOG(gPageCacheLog, LogLevel::Debug, + ("resize widget(%d, %d, %d, %d)", newBounds.x, newBounds.y, + newBounds.width, newBounds.height)); + mContentViewer->SetBounds(newBounds); + } else { + nsIScrollableFrame* rootScrollFrame = + presShell->GetRootScrollFrameAsScrollable(); + if (rootScrollFrame) { + rootScrollFrame->PostScrolledAreaEventForCurrentArea(); + } + } + } + + // The FinishRestore call below can kill these, null them out so we don't + // have invalid pointer lying around. + newRootView = rootViewSibling = rootViewParent = nullptr; + newVM = nullptr; + + // If the IsUnderHiddenEmbedderElement() state has been changed, we need to + // update it. + if (oldPresShell && presShell && + presShell->IsUnderHiddenEmbedderElement() != + oldPresShell->IsUnderHiddenEmbedderElement()) { + presShell->SetIsUnderHiddenEmbedderElement( + oldPresShell->IsUnderHiddenEmbedderElement()); + } + + // Simulate the completion of the load. + nsDocShell::FinishRestore(); + + // Restart plugins, and paint the content. + if (presShell) { + presShell->Thaw(); + } + + return privWin->FireDelayedDOMEvents(true); +} + +nsresult nsDocShell::CreateContentViewer(const nsACString& aContentType, + nsIRequest* aRequest, + nsIStreamListener** aContentHandler) { + if (DocGroup::TryToLoadIframesInBackground()) { + ResetToFirstLoad(); + } + + *aContentHandler = nullptr; + + if (!mTreeOwner || mIsBeingDestroyed) { + // If we don't have a tree owner, then we're in the process of being + // destroyed. Rather than continue trying to load something, just give up. + return NS_ERROR_DOCSHELL_DYING; + } + + if (!mBrowsingContext->AncestorsAreCurrent() || + mBrowsingContext->IsInBFCache()) { + mBrowsingContext->RemoveRootFromBFCacheSync(); + return NS_ERROR_NOT_AVAILABLE; + } + + // Can we check the content type of the current content viewer + // and reuse it without destroying it and re-creating it? + + NS_ASSERTION(mLoadGroup, "Someone ignored return from Init()?"); + + // Instantiate the content viewer object + nsCOMPtr<nsIContentViewer> viewer; + nsresult rv = NewContentViewerObj(aContentType, aRequest, mLoadGroup, + aContentHandler, getter_AddRefs(viewer)); + + if (NS_FAILED(rv)) { + return rv; + } + + // Notify the current document that it is about to be unloaded!! + // + // It is important to fire the unload() notification *before* any state + // is changed within the DocShell - otherwise, javascript will get the + // wrong information :-( + // + + if (mSavingOldViewer) { + // We determined that it was safe to cache the document presentation + // at the time we initiated the new load. We need to check whether + // it's still safe to do so, since there may have been DOM mutations + // or new requests initiated. + RefPtr<Document> doc = viewer->GetDocument(); + mSavingOldViewer = CanSavePresentation( + mLoadType, aRequest, doc, /* aReportBFCacheComboTelemetry */ false); + } + + NS_ASSERTION(!mLoadingURI, "Re-entering unload?"); + + nsCOMPtr<nsIChannel> aOpenedChannel = do_QueryInterface(aRequest); + if (aOpenedChannel) { + aOpenedChannel->GetURI(getter_AddRefs(mLoadingURI)); + } + + // Grab the current URI, we need to pass it to Embed, and OnNewURI will reset + // it before we do call Embed. + nsCOMPtr<nsIURI> previousURI = mCurrentURI; + + FirePageHideNotification(!mSavingOldViewer); + if (mIsBeingDestroyed) { + // Force to stop the newly created orphaned viewer. + viewer->Stop(); + return NS_ERROR_DOCSHELL_DYING; + } + mLoadingURI = nullptr; + + // Set mFiredUnloadEvent = false so that the unload handler for the + // *new* document will fire. + mFiredUnloadEvent = false; + + // we've created a new document so go ahead and call + // OnNewURI(), but don't fire OnLocationChange() + // notifications before we've called Embed(). See bug 284993. + mURIResultedInDocument = true; + bool errorOnLocationChangeNeeded = false; + nsCOMPtr<nsIChannel> failedChannel = mFailedChannel; + nsCOMPtr<nsIURI> failedURI; + + if (mLoadType == LOAD_ERROR_PAGE) { + // We need to set the SH entry and our current URI here and not + // at the moment we load the page. We want the same behavior + // of Stop() as for a normal page load. See bug 514232 for details. + + // Revert mLoadType to load type to state the page load failed, + // following function calls need it. + mLoadType = mFailedLoadType; + + Document* doc = viewer->GetDocument(); + if (doc) { + doc->SetFailedChannel(failedChannel); + } + + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + if (failedChannel) { + // Make sure we have a URI to set currentURI. + NS_GetFinalChannelURI(failedChannel, getter_AddRefs(failedURI)); + } else { + // if there is no failed channel we have to explicitly provide + // a triggeringPrincipal for the history entry. + triggeringPrincipal = nsContentUtils::GetSystemPrincipal(); + } + + if (!failedURI) { + failedURI = mFailedURI; + } + if (!failedURI) { + // We need a URI object to store a session history entry, so make up a URI + NS_NewURI(getter_AddRefs(failedURI), "about:blank"); + } + + // When we don't have failedURI, something wrong will happen. See + // bug 291876. + MOZ_ASSERT(failedURI, "We don't have a URI for history APIs."); + + mFailedChannel = nullptr; + mFailedURI = nullptr; + + // Create an shistory entry for the old load. + if (failedURI) { + errorOnLocationChangeNeeded = + OnNewURI(failedURI, failedChannel, triggeringPrincipal, nullptr, + nullptr, nullptr, false, false, false); + } + + // Be sure to have a correct mLSHE, it may have been cleared by + // EndPageLoad. See bug 302115. + ChildSHistory* shistory = GetSessionHistory(); + if (!mozilla::SessionHistoryInParent() && shistory && !mLSHE) { + int32_t idx = shistory->LegacySHistory()->GetRequestedIndex(); + if (idx == -1) { + idx = shistory->Index(); + } + shistory->LegacySHistory()->GetEntryAtIndex(idx, getter_AddRefs(mLSHE)); + } + + mLoadType = LOAD_ERROR_PAGE; + } + + nsCOMPtr<nsIURI> finalURI; + // If this a redirect, use the final url (uri) + // else use the original url + // + // Note that this should match what documents do (see Document::Reset). + NS_GetFinalChannelURI(aOpenedChannel, getter_AddRefs(finalURI)); + + bool onLocationChangeNeeded = false; + if (finalURI) { + // Pass false for aCloneSHChildren, since we're loading a new page here. + onLocationChangeNeeded = + OnNewURI(finalURI, aOpenedChannel, nullptr, nullptr, nullptr, nullptr, + false, true, false); + } + + // let's try resetting the load group if we need to... + nsCOMPtr<nsILoadGroup> currentLoadGroup; + NS_ENSURE_SUCCESS( + aOpenedChannel->GetLoadGroup(getter_AddRefs(currentLoadGroup)), + NS_ERROR_FAILURE); + + if (currentLoadGroup != mLoadGroup) { + nsLoadFlags loadFlags = 0; + + // Cancel any URIs that are currently loading... + // XXX: Need to do this eventually Stop(); + // + // Retarget the document to this loadgroup... + // + /* First attach the channel to the right loadgroup + * and then remove from the old loadgroup. This + * puts the notifications in the right order and + * we don't null-out mLSHE in OnStateChange() for + * all redirected urls + */ + aOpenedChannel->SetLoadGroup(mLoadGroup); + + // Mark the channel as being a document URI... + aOpenedChannel->GetLoadFlags(&loadFlags); + loadFlags |= nsIChannel::LOAD_DOCUMENT_URI; + nsCOMPtr<nsILoadInfo> loadInfo = aOpenedChannel->LoadInfo(); + if (SandboxFlagsImplyCookies(loadInfo->GetSandboxFlags())) { + loadFlags |= nsIRequest::LOAD_DOCUMENT_NEEDS_COOKIE; + } + + aOpenedChannel->SetLoadFlags(loadFlags); + + mLoadGroup->AddRequest(aRequest, nullptr); + if (currentLoadGroup) { + currentLoadGroup->RemoveRequest(aRequest, nullptr, NS_BINDING_RETARGETED); + } + + // Update the notification callbacks, so that progress and + // status information are sent to the right docshell... + aOpenedChannel->SetNotificationCallbacks(this); + } + + if (DocGroup::TryToLoadIframesInBackground()) { + if ((!mContentViewer || GetDocument()->IsInitialDocument()) && + IsSubframe()) { + // At this point, we know we just created a new iframe document based on + // the response from the server, and we check if it's a cross-domain + // iframe + + RefPtr<Document> newDoc = viewer->GetDocument(); + + RefPtr<nsDocShell> parent = GetInProcessParentDocshell(); + nsCOMPtr<nsIPrincipal> parentPrincipal = + parent->GetDocument()->NodePrincipal(); + nsCOMPtr<nsIPrincipal> thisPrincipal = newDoc->NodePrincipal(); + + SiteIdentifier parentSite; + SiteIdentifier thisSite; + + nsresult rv = + BasePrincipal::Cast(parentPrincipal)->GetSiteIdentifier(parentSite); + NS_ENSURE_SUCCESS(rv, rv); + + rv = BasePrincipal::Cast(thisPrincipal)->GetSiteIdentifier(thisSite); + NS_ENSURE_SUCCESS(rv, rv); + + if (!parentSite.Equals(thisSite)) { + if (profiler_thread_is_being_profiled_for_markers()) { + nsCOMPtr<nsIURI> prinURI; + BasePrincipal::Cast(thisPrincipal)->GetURI(getter_AddRefs(prinURI)); + nsPrintfCString marker( + "Iframe loaded in background: %s", + nsContentUtils::TruncatedURLForDisplay(prinURI).get()); + PROFILER_MARKER_TEXT("Background Iframe", DOM, {}, marker); + } + SetBackgroundLoadIframe(); + } + } + } + + NS_ENSURE_SUCCESS(Embed(viewer, nullptr, false, + ShouldAddToSessionHistory(finalURI, aOpenedChannel), + aOpenedChannel, previousURI), + NS_ERROR_FAILURE); + + if (!mBrowsingContext->GetHasLoadedNonInitialDocument()) { + MOZ_ALWAYS_SUCCEEDS(mBrowsingContext->SetHasLoadedNonInitialDocument(true)); + } + + if (TreatAsBackgroundLoad()) { + nsCOMPtr<nsIRunnable> triggerParentCheckDocShell = + NewRunnableMethod("nsDocShell::TriggerParentCheckDocShellIsEmpty", this, + &nsDocShell::TriggerParentCheckDocShellIsEmpty); + nsresult rv = NS_DispatchToCurrentThread(triggerParentCheckDocShell); + NS_ENSURE_SUCCESS(rv, rv); + } + + mSavedRefreshURIList = nullptr; + mSavingOldViewer = false; + mEODForCurrentDocument = false; + + // if this document is part of a multipart document, + // the ID can be used to distinguish it from the other parts. + nsCOMPtr<nsIMultiPartChannel> multiPartChannel(do_QueryInterface(aRequest)); + if (multiPartChannel) { + if (PresShell* presShell = GetPresShell()) { + if (Document* doc = presShell->GetDocument()) { + uint32_t partID; + multiPartChannel->GetPartID(&partID); + doc->SetPartID(partID); + } + } + } + + if (errorOnLocationChangeNeeded) { + FireOnLocationChange(this, failedChannel, failedURI, + LOCATION_CHANGE_ERROR_PAGE); + } else if (onLocationChangeNeeded) { + uint32_t locationFlags = + (mLoadType & LOAD_CMD_RELOAD) ? uint32_t(LOCATION_CHANGE_RELOAD) : 0; + FireOnLocationChange(this, aRequest, mCurrentURI, locationFlags); + } + + return NS_OK; +} + +nsresult nsDocShell::NewContentViewerObj(const nsACString& aContentType, + nsIRequest* aRequest, + nsILoadGroup* aLoadGroup, + nsIStreamListener** aContentHandler, + nsIContentViewer** aViewer) { + nsCOMPtr<nsIChannel> aOpenedChannel = do_QueryInterface(aRequest); + + nsCOMPtr<nsIDocumentLoaderFactory> docLoaderFactory = + nsContentUtils::FindInternalContentViewer(aContentType); + if (!docLoaderFactory) { + return NS_ERROR_FAILURE; + } + + // Now create an instance of the content viewer nsLayoutDLF makes the + // determination if it should be a "view-source" instead of "view" + nsresult rv = docLoaderFactory->CreateInstance( + "view", aOpenedChannel, aLoadGroup, aContentType, this, nullptr, + aContentHandler, aViewer); + NS_ENSURE_SUCCESS(rv, rv); + + (*aViewer)->SetContainer(this); + return NS_OK; +} + +nsresult nsDocShell::SetupNewViewer(nsIContentViewer* aNewViewer, + WindowGlobalChild* aWindowActor) { + MOZ_ASSERT(!mIsBeingDestroyed); + + // + // Copy content viewer state from previous or parent content viewer. + // + // The following logic is mirrored in nsHTMLDocument::StartDocumentLoad! + // + // Do NOT to maintain a reference to the old content viewer outside + // of this "copying" block, or it will not be destroyed until the end of + // this routine and all <SCRIPT>s and event handlers fail! (bug 20315) + // + // In this block of code, if we get an error result, we return it + // but if we get a null pointer, that's perfectly legal for parent + // and parentContentViewer. + // + + int32_t x = 0; + int32_t y = 0; + int32_t cx = 0; + int32_t cy = 0; + + // This will get the size from the current content viewer or from the + // Init settings + DoGetPositionAndSize(&x, &y, &cx, &cy); + + nsCOMPtr<nsIDocShellTreeItem> parentAsItem; + NS_ENSURE_SUCCESS(GetInProcessSameTypeParent(getter_AddRefs(parentAsItem)), + NS_ERROR_FAILURE); + nsCOMPtr<nsIDocShell> parent(do_QueryInterface(parentAsItem)); + + const Encoding* reloadEncoding = nullptr; + int32_t reloadEncodingSource = kCharsetUninitialized; + // |newMUDV| also serves as a flag to set the data from the above vars + nsCOMPtr<nsIContentViewer> newCv; + + if (mContentViewer || parent) { + nsCOMPtr<nsIContentViewer> oldCv; + if (mContentViewer) { + // Get any interesting state from old content viewer + // XXX: it would be far better to just reuse the document viewer , + // since we know we're just displaying the same document as before + oldCv = mContentViewer; + + // Tell the old content viewer to hibernate in session history when + // it is destroyed. + + if (mSavingOldViewer && NS_FAILED(CaptureState())) { + if (mOSHE) { + mOSHE->SyncPresentationState(); + } + mSavingOldViewer = false; + } + } else { + // No old content viewer, so get state from parent's content viewer + parent->GetContentViewer(getter_AddRefs(oldCv)); + } + + if (oldCv) { + newCv = aNewViewer; + if (newCv) { + reloadEncoding = + oldCv->GetReloadEncodingAndSource(&reloadEncodingSource); + } + } + } + + nscolor bgcolor = NS_RGBA(0, 0, 0, 0); + bool isUnderHiddenEmbedderElement = false; + // Ensure that the content viewer is destroyed *after* the GC - bug 71515 + nsCOMPtr<nsIContentViewer> contentViewer = mContentViewer; + if (contentViewer) { + // Stop any activity that may be happening in the old document before + // releasing it... + contentViewer->Stop(); + + // Try to extract the canvas background color from the old + // presentation shell, so we can use it for the next document. + if (PresShell* presShell = contentViewer->GetPresShell()) { + bgcolor = presShell->GetCanvasBackground(); + isUnderHiddenEmbedderElement = presShell->IsUnderHiddenEmbedderElement(); + } + + contentViewer->Close(mSavingOldViewer ? mOSHE.get() : nullptr); + aNewViewer->SetPreviousViewer(contentViewer); + } + if (mOSHE && (!mContentViewer || !mSavingOldViewer)) { + // We don't plan to save a viewer in mOSHE; tell it to drop + // any other state it's holding. + mOSHE->SyncPresentationState(); + } + + mContentViewer = nullptr; + + // Now that we're about to switch documents, forget all of our children. + // Note that we cached them as needed up in CaptureState above. + DestroyChildren(); + + mContentViewer = aNewViewer; + + nsCOMPtr<nsIWidget> widget; + NS_ENSURE_SUCCESS(GetMainWidget(getter_AddRefs(widget)), NS_ERROR_FAILURE); + + nsIntRect bounds(x, y, cx, cy); + + mContentViewer->SetNavigationTiming(mTiming); + + if (NS_FAILED(mContentViewer->Init(widget, bounds, aWindowActor))) { + nsCOMPtr<nsIContentViewer> viewer = mContentViewer; + viewer->Close(nullptr); + viewer->Destroy(); + mContentViewer = nullptr; + mCurrentURI = nullptr; + NS_WARNING("ContentViewer Initialization failed"); + return NS_ERROR_FAILURE; + } + + // If we have old state to copy, set the old state onto the new content + // viewer + if (newCv) { + newCv->SetReloadEncodingAndSource(reloadEncoding, reloadEncodingSource); + } + + NS_ENSURE_TRUE(mContentViewer, NS_ERROR_FAILURE); + + // Stuff the bgcolor from the old pres shell into the new + // pres shell. This improves page load continuity. + if (RefPtr<PresShell> presShell = mContentViewer->GetPresShell()) { + presShell->SetCanvasBackground(bgcolor); + presShell->ActivenessMaybeChanged(); + if (isUnderHiddenEmbedderElement) { + presShell->SetIsUnderHiddenEmbedderElement(isUnderHiddenEmbedderElement); + } + } + + // XXX: It looks like the LayoutState gets restored again in Embed() + // right after the call to SetupNewViewer(...) + + // We don't show the mContentViewer yet, since we want to draw the old page + // until we have enough of the new page to show. Just return with the new + // viewer still set to hidden. + + return NS_OK; +} + +void nsDocShell::SetDocCurrentStateObj(nsISHEntry* aShEntry, + SessionHistoryInfo* aInfo) { + NS_ENSURE_TRUE_VOID(mContentViewer); + + RefPtr<Document> document = GetDocument(); + NS_ENSURE_TRUE_VOID(document); + + nsCOMPtr<nsIStructuredCloneContainer> scContainer; + if (mozilla::SessionHistoryInParent()) { + // If aInfo is null, just set the document's state object to null. + if (aInfo) { + scContainer = aInfo->GetStateData(); + } + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p SetCurrentDocState %p", this, scContainer.get())); + } else { + if (aShEntry) { + scContainer = aShEntry->GetStateData(); + + // If aShEntry is null, just set the document's state object to null. + } + } + + // It's OK for scContainer too be null here; that just means there's no + // state data associated with this history entry. + document->SetStateObject(scContainer); +} + +nsresult nsDocShell::CheckLoadingPermissions() { + // This method checks whether the caller may load content into + // this docshell. Even though we've done our best to hide windows + // from code that doesn't have the right to access them, it's + // still possible for an evil site to open a window and access + // frames in the new window through window.frames[] (which is + // allAccess for historic reasons), so we still need to do this + // check on load. + nsresult rv = NS_OK; + + if (!IsSubframe()) { + // We're not a frame. Permit all loads. + return rv; + } + + // Note - The check for a current JSContext here isn't necessarily sensical. + // It's just designed to preserve the old semantics during a mass-conversion + // patch. + if (!nsContentUtils::GetCurrentJSContext()) { + return NS_OK; + } + + // Check if the caller is from the same origin as this docshell, + // or any of its ancestors. + for (RefPtr<BrowsingContext> bc = mBrowsingContext; bc; + bc = bc->GetParent()) { + // If the BrowsingContext is not in process, then it + // is true by construction that its principal will not + // subsume the current docshell principal. + if (!bc->IsInProcess()) { + continue; + } + + nsCOMPtr<nsIScriptGlobalObject> sgo = + bc->GetDocShell()->GetScriptGlobalObject(); + nsCOMPtr<nsIScriptObjectPrincipal> sop(do_QueryInterface(sgo)); + + nsIPrincipal* p; + if (!sop || !(p = sop->GetPrincipal())) { + return NS_ERROR_UNEXPECTED; + } + + if (nsContentUtils::SubjectPrincipal()->Subsumes(p)) { + // Same origin, permit load + return NS_OK; + } + } + + return NS_ERROR_DOM_PROP_ACCESS_DENIED; +} + +//***************************************************************************** +// nsDocShell: Site Loading +//***************************************************************************** + +void nsDocShell::CopyFavicon(nsIURI* aOldURI, nsIURI* aNewURI, + bool aInPrivateBrowsing) { + if (XRE_IsContentProcess()) { + dom::ContentChild* contentChild = dom::ContentChild::GetSingleton(); + if (contentChild) { + contentChild->SendCopyFavicon(aOldURI, aNewURI, aInPrivateBrowsing); + } + return; + } + +#ifdef MOZ_PLACES + nsCOMPtr<nsIFaviconService> favSvc = + do_GetService("@mozilla.org/browser/favicon-service;1"); + if (favSvc) { + favSvc->CopyFavicons(aOldURI, aNewURI, + aInPrivateBrowsing + ? nsIFaviconService::FAVICON_LOAD_PRIVATE + : nsIFaviconService::FAVICON_LOAD_NON_PRIVATE, + nullptr); + } +#endif +} + +class InternalLoadEvent : public Runnable { + public: + InternalLoadEvent(nsDocShell* aDocShell, nsDocShellLoadState* aLoadState) + : mozilla::Runnable("InternalLoadEvent"), + mDocShell(aDocShell), + mLoadState(aLoadState) { + // For events, both target and filename should be the version of "null" they + // expect. By the time the event is fired, both window targeting and file + // downloading have been handled, so we should never have an internal load + // event that retargets or had a download. + mLoadState->SetTarget(u""_ns); + mLoadState->SetFileName(VoidString()); + } + + NS_IMETHOD + Run() override { +#ifndef ANDROID + MOZ_ASSERT(mLoadState->TriggeringPrincipal(), + "InternalLoadEvent: Should always have a principal here"); +#endif + return mDocShell->InternalLoad(mLoadState); + } + + private: + RefPtr<nsDocShell> mDocShell; + RefPtr<nsDocShellLoadState> mLoadState; +}; + +/** + * Returns true if we started an asynchronous load (i.e., from the network), but + * the document we're loading there hasn't yet become this docshell's active + * document. + * + * When JustStartedNetworkLoad is true, you should be careful about modifying + * mLoadType and mLSHE. These are both set when the asynchronous load first + * starts, and the load expects that, when it eventually runs InternalLoad, + * mLoadType and mLSHE will have their original values. + */ +bool nsDocShell::JustStartedNetworkLoad() { + return mDocumentRequest && mDocumentRequest != GetCurrentDocChannel(); +} + +// The contentType will be INTERNAL_(I)FRAME if this docshell is for a +// non-toplevel browsing context in spec terms. (frame, iframe, <object>, +// <embed>, etc) +// +// This return value will be used when we call NS_CheckContentLoadPolicy, and +// later when we call DoURILoad. +nsContentPolicyType nsDocShell::DetermineContentType() { + if (!IsSubframe()) { + return nsIContentPolicy::TYPE_DOCUMENT; + } + + const auto& maybeEmbedderElementType = + GetBrowsingContext()->GetEmbedderElementType(); + if (!maybeEmbedderElementType) { + // If the EmbedderElementType hasn't been set yet, just assume we're + // an iframe since that's more common. + return nsIContentPolicy::TYPE_INTERNAL_IFRAME; + } + + return maybeEmbedderElementType->EqualsLiteral("iframe") + ? nsIContentPolicy::TYPE_INTERNAL_IFRAME + : nsIContentPolicy::TYPE_INTERNAL_FRAME; +} + +bool nsDocShell::NoopenerForceEnabled() { + // If current's top-level browsing context's active document's + // cross-origin-opener-policy is "same-origin" or "same-origin + COEP" then + // if currentDoc's origin is not same origin with currentDoc's top-level + // origin, noopener is force enabled, and name is cleared to "_blank". + auto topPolicy = mBrowsingContext->Top()->GetOpenerPolicy(); + return (topPolicy == nsILoadInfo::OPENER_POLICY_SAME_ORIGIN || + topPolicy == + nsILoadInfo:: + OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP) && + !mBrowsingContext->SameOriginWithTop(); +} + +nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { + MOZ_ASSERT(aLoadState, "need a load state!"); + MOZ_ASSERT(!aLoadState->Target().IsEmpty(), "should have a target here!"); + MOZ_ASSERT(aLoadState->TargetBrowsingContext().IsNull(), + "should not have picked target yet"); + + nsresult rv = NS_OK; + RefPtr<BrowsingContext> targetContext; + + // Only _self, _parent, and _top are supported in noopener case. But we + // have to be careful to not apply that to the noreferrer case. See bug + // 1358469. + bool allowNamedTarget = + !aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_NO_OPENER) || + aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER); + if (allowNamedTarget || + aLoadState->Target().LowerCaseEqualsLiteral("_self") || + aLoadState->Target().LowerCaseEqualsLiteral("_parent") || + aLoadState->Target().LowerCaseEqualsLiteral("_top")) { + Document* document = GetDocument(); + NS_ENSURE_TRUE(document, NS_ERROR_FAILURE); + WindowGlobalChild* wgc = document->GetWindowGlobalChild(); + NS_ENSURE_TRUE(wgc, NS_ERROR_FAILURE); + targetContext = wgc->FindBrowsingContextWithName( + aLoadState->Target(), /* aUseEntryGlobalForAccessCheck */ false); + } + + if (!targetContext) { + // If the targetContext doesn't exist, then this is a new docShell and we + // should consider this a TYPE_DOCUMENT load + // + // For example, when target="_blank" + + // If there's no targetContext, that means we are about to create a new + // window. Perform a content policy check before creating the window. Please + // note for all other docshell loads content policy checks are performed + // within the contentSecurityManager when the channel is about to be + // openend. + nsISupports* requestingContext = nullptr; + if (XRE_IsContentProcess()) { + // In e10s the child process doesn't have access to the element that + // contains the browsing context (because that element is in the chrome + // process). So we just pass mScriptGlobal. + requestingContext = ToSupports(mScriptGlobal); + } else { + // This is for loading non-e10s tabs and toplevel windows of various + // sorts. + // For the toplevel window cases, requestingElement will be null. + nsCOMPtr<Element> requestingElement = + mScriptGlobal->GetFrameElementInternal(); + requestingContext = requestingElement; + } + + // Ideally we should use the same loadinfo as within DoURILoad which + // should match this one when both are applicable. + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = + new LoadInfo(mScriptGlobal, aLoadState->URI(), + aLoadState->TriggeringPrincipal(), requestingContext, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, 0); + + // Since Content Policy checks are performed within docShell as well as + // the ContentSecurityManager we need a reliable way to let certain + // nsIContentPolicy consumers ignore duplicate calls. + secCheckLoadInfo->SetSkipContentPolicyCheckForWebRequest(true); + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(aLoadState->URI(), secCheckLoadInfo, + ""_ns, // mime guess + &shouldLoad); + + if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { + if (NS_SUCCEEDED(rv)) { + if (shouldLoad == nsIContentPolicy::REJECT_TYPE) { + return NS_ERROR_CONTENT_BLOCKED_SHOW_ALT; + } + if (shouldLoad == nsIContentPolicy::REJECT_POLICY) { + return NS_ERROR_BLOCKED_BY_POLICY; + } + } + + return NS_ERROR_CONTENT_BLOCKED; + } + } + + // + // Resolve the window target before going any further... + // If the load has been targeted to another DocShell, then transfer the + // load to it... + // + + // We've already done our owner-inheriting. Mask out that bit, so we + // don't try inheriting an owner from the target window if we came up + // with a null owner above. + aLoadState->UnsetInternalLoadFlag(INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL); + + if (!targetContext) { + // If the docshell's document is sandboxed, only open a new window + // if the document's SANDBOXED_AUXILLARY_NAVIGATION flag is not set. + // (i.e. if allow-popups is specified) + NS_ENSURE_TRUE(mContentViewer, NS_ERROR_FAILURE); + Document* doc = mContentViewer->GetDocument(); + + const bool isDocumentAuxSandboxed = + doc && (doc->GetSandboxFlags() & SANDBOXED_AUXILIARY_NAVIGATION); + + if (isDocumentAuxSandboxed) { + return NS_ERROR_DOM_INVALID_ACCESS_ERR; + } + + nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow(); + NS_ENSURE_TRUE(win, NS_ERROR_NOT_AVAILABLE); + + RefPtr<BrowsingContext> newBC; + nsAutoCString spec; + aLoadState->URI()->GetSpec(spec); + + // If we are a noopener load, we just hand the whole thing over to our + // window. + if (aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_NO_OPENER) || + NoopenerForceEnabled()) { + // Various asserts that we know to hold because NO_OPENER loads can only + // happen for links. + MOZ_ASSERT(!aLoadState->LoadReplace()); + MOZ_ASSERT(aLoadState->PrincipalToInherit() == + aLoadState->TriggeringPrincipal()); + MOZ_ASSERT(!(aLoadState->InternalLoadFlags() & + ~(INTERNAL_LOAD_FLAGS_NO_OPENER | + INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER)), + "Only INTERNAL_LOAD_FLAGS_NO_OPENER and " + "INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER can be set"); + MOZ_ASSERT_IF(aLoadState->PostDataStream(), + aLoadState->IsFormSubmission()); + MOZ_ASSERT(!aLoadState->HeadersStream()); + // If OnLinkClickSync was invoked inside the onload handler, the load + // type would be set to LOAD_NORMAL_REPLACE; otherwise it should be + // LOAD_LINK. + MOZ_ASSERT(aLoadState->LoadType() == LOAD_LINK || + aLoadState->LoadType() == LOAD_NORMAL_REPLACE); + MOZ_ASSERT(!aLoadState->LoadIsFromSessionHistory()); + MOZ_ASSERT(aLoadState->FirstParty()); // Windowwatcher will assume this. + + RefPtr<nsDocShellLoadState> loadState = + new nsDocShellLoadState(aLoadState->URI()); + + // Set up our loadinfo so it will do the load as much like we would have + // as possible. + loadState->SetReferrerInfo(aLoadState->GetReferrerInfo()); + loadState->SetOriginalURI(aLoadState->OriginalURI()); + + Maybe<nsCOMPtr<nsIURI>> resultPrincipalURI; + aLoadState->GetMaybeResultPrincipalURI(resultPrincipalURI); + + loadState->SetMaybeResultPrincipalURI(resultPrincipalURI); + loadState->SetKeepResultPrincipalURIIfSet( + aLoadState->KeepResultPrincipalURIIfSet()); + // LoadReplace will always be false due to asserts above, skip setting + // it. + loadState->SetTriggeringPrincipal(aLoadState->TriggeringPrincipal()); + loadState->SetTriggeringSandboxFlags( + aLoadState->TriggeringSandboxFlags()); + loadState->SetCsp(aLoadState->Csp()); + loadState->SetInheritPrincipal(aLoadState->HasInternalLoadFlags( + INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL)); + // Explicit principal because we do not want any guesses as to what the + // principal to inherit is: it should be aTriggeringPrincipal. + loadState->SetPrincipalIsExplicit(true); + loadState->SetLoadType(LOAD_LINK); + loadState->SetForceAllowDataURI(aLoadState->HasInternalLoadFlags( + INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI)); + + loadState->SetHasValidUserGestureActivation( + aLoadState->HasValidUserGestureActivation()); + + // Propagate POST data to the new load. + loadState->SetPostDataStream(aLoadState->PostDataStream()); + loadState->SetIsFormSubmission(aLoadState->IsFormSubmission()); + + rv = win->Open(NS_ConvertUTF8toUTF16(spec), + aLoadState->Target(), // window name + u""_ns, // Features + loadState, + true, // aForceNoOpener + getter_AddRefs(newBC)); + MOZ_ASSERT(!newBC); + return rv; + } + + rv = win->OpenNoNavigate(NS_ConvertUTF8toUTF16(spec), + aLoadState->Target(), // window name + u""_ns, // Features + getter_AddRefs(newBC)); + + // In some cases the Open call doesn't actually result in a new + // window being opened. We can detect these cases by examining the + // document in |newBC|, if any. + nsCOMPtr<nsPIDOMWindowOuter> piNewWin = + newBC ? newBC->GetDOMWindow() : nullptr; + if (piNewWin) { + RefPtr<Document> newDoc = piNewWin->GetExtantDoc(); + if (!newDoc || newDoc->IsInitialDocument()) { + aLoadState->SetInternalLoadFlag(INTERNAL_LOAD_FLAGS_FIRST_LOAD); + } + } + + if (newBC) { + targetContext = newBC; + } + } + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(targetContext, rv); + + // If our target BrowsingContext is still pending initialization, ignore the + // navigation request targeting it. + if (NS_WARN_IF(targetContext->GetPendingInitialization())) { + return NS_OK; + } + + aLoadState->SetTargetBrowsingContext(targetContext); + // + // Transfer the load to the target BrowsingContext... Clear the window target + // name to the empty string to prevent recursive retargeting! + // + // No window target + aLoadState->SetTarget(u""_ns); + // No forced download + aLoadState->SetFileName(VoidString()); + return targetContext->InternalLoad(aLoadState); +} + +static nsAutoCString RefMaybeNull(nsIURI* aURI) { + nsAutoCString result; + if (NS_FAILED(aURI->GetRef(result))) { + result.SetIsVoid(true); + } + return result; +} + +uint32_t nsDocShell::GetSameDocumentNavigationFlags(nsIURI* aNewURI) { + uint32_t flags = LOCATION_CHANGE_SAME_DOCUMENT; + + bool equal = false; + if (mCurrentURI && + NS_SUCCEEDED(mCurrentURI->EqualsExceptRef(aNewURI, &equal)) && equal && + RefMaybeNull(mCurrentURI) != RefMaybeNull(aNewURI)) { + flags |= LOCATION_CHANGE_HASHCHANGE; + } + + return flags; +} + +bool nsDocShell::IsSameDocumentNavigation(nsDocShellLoadState* aLoadState, + SameDocumentNavigationState& aState) { + MOZ_ASSERT(aLoadState); + if (!(aLoadState->LoadType() == LOAD_NORMAL || + aLoadState->LoadType() == LOAD_STOP_CONTENT || + LOAD_TYPE_HAS_FLAGS(aLoadState->LoadType(), + LOAD_FLAGS_REPLACE_HISTORY) || + aLoadState->LoadType() == LOAD_HISTORY || + aLoadState->LoadType() == LOAD_LINK)) { + return false; + } + + nsCOMPtr<nsIURI> currentURI = mCurrentURI; + + nsresult rvURINew = aLoadState->URI()->GetRef(aState.mNewHash); + if (NS_SUCCEEDED(rvURINew)) { + rvURINew = aLoadState->URI()->GetHasRef(&aState.mNewURIHasRef); + } + + if (currentURI && NS_SUCCEEDED(rvURINew)) { + nsresult rvURIOld = currentURI->GetRef(aState.mCurrentHash); + if (NS_SUCCEEDED(rvURIOld)) { + rvURIOld = currentURI->GetHasRef(&aState.mCurrentURIHasRef); + } + if (NS_SUCCEEDED(rvURIOld)) { + if (NS_FAILED(currentURI->EqualsExceptRef(aLoadState->URI(), + &aState.mSameExceptHashes))) { + aState.mSameExceptHashes = false; + } + } + } + + if (!aState.mSameExceptHashes && currentURI && NS_SUCCEEDED(rvURINew)) { + // Maybe aLoadState->URI() came from the exposable form of currentURI? + nsCOMPtr<nsIURI> currentExposableURI = + nsIOService::CreateExposableURI(currentURI); + nsresult rvURIOld = currentExposableURI->GetRef(aState.mCurrentHash); + if (NS_SUCCEEDED(rvURIOld)) { + rvURIOld = currentExposableURI->GetHasRef(&aState.mCurrentURIHasRef); + } + if (NS_SUCCEEDED(rvURIOld)) { + if (NS_FAILED(currentExposableURI->EqualsExceptRef( + aLoadState->URI(), &aState.mSameExceptHashes))) { + aState.mSameExceptHashes = false; + } + // HTTPS-Only Mode upgrades schemes from http to https in Necko, hence we + // have to perform a special check here to avoid an actual navigation. If + // HTTPS-Only Mode is enabled and the two URIs are same-origin (modulo the + // fact that the new URI is currently http), then set mSameExceptHashes to + // true and only perform a fragment navigation. + if (!aState.mSameExceptHashes) { + if (nsCOMPtr<nsIChannel> docChannel = GetCurrentDocChannel()) { + nsCOMPtr<nsILoadInfo> docLoadInfo = docChannel->LoadInfo(); + if (!docLoadInfo->GetLoadErrorPage() && + nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef( + currentExposableURI, aLoadState->URI(), docLoadInfo)) { + uint32_t status = docLoadInfo->GetHttpsOnlyStatus(); + if (status & (nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED | + nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST)) { + // At this point the requested URI is for sure a fragment + // navigation via HTTP and HTTPS-Only mode or HTTPS-First is + // enabled. Also it is not interfering the upgrade order of + // https://searchfox.org/mozilla-central/source/netwerk/base/nsNetUtil.cpp#2948-2953. + // Since we are on an HTTPS site the fragment + // navigation should also be an HTTPS. + // For that reason we should upgrade the URI to HTTPS. + aState.mSecureUpgradeURI = true; + aState.mSameExceptHashes = true; + } + } + } + } + } + } + + if (mozilla::SessionHistoryInParent()) { + if (mActiveEntry && aLoadState->LoadIsFromSessionHistory()) { + aState.mHistoryNavBetweenSameDoc = mActiveEntry->SharesDocumentWith( + aLoadState->GetLoadingSessionHistoryInfo()->mInfo); + } + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell::IsSameDocumentNavigation %p NavBetweenSameDoc=%d", + this, aState.mHistoryNavBetweenSameDoc)); + } else { + if (mOSHE && aLoadState->LoadIsFromSessionHistory()) { + // We're doing a history load. + + mOSHE->SharesDocumentWith(aLoadState->SHEntry(), + &aState.mHistoryNavBetweenSameDoc); + } + } + + // A same document navigation happens when we navigate between two SHEntries + // for the same document. We do a same document navigation under two + // circumstances. Either + // + // a) we're navigating between two different SHEntries which share a + // document, or + // + // b) we're navigating to a new shentry whose URI differs from the + // current URI only in its hash, the new hash is non-empty, and + // we're not doing a POST. + // + // The restriction that the SHEntries in (a) must be different ensures + // that history.go(0) and the like trigger full refreshes, rather than + // same document navigations. + if (!mozilla::SessionHistoryInParent()) { + bool doSameDocumentNavigation = + (aState.mHistoryNavBetweenSameDoc && mOSHE != aLoadState->SHEntry()) || + (!aLoadState->SHEntry() && !aLoadState->PostDataStream() && + aState.mSameExceptHashes && aState.mNewURIHasRef); + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p NavBetweenSameDoc=%d is same doc = %d", this, + aState.mHistoryNavBetweenSameDoc, doSameDocumentNavigation)); + return doSameDocumentNavigation; + } + + if (aState.mHistoryNavBetweenSameDoc && + !aLoadState->GetLoadingSessionHistoryInfo()->mLoadingCurrentEntry) { + return true; + } + + MOZ_LOG( + gSHLog, LogLevel::Debug, + ("nsDocShell::IsSameDocumentNavigation %p !LoadIsFromSessionHistory=%s " + "!PostDataStream: %s mSameExceptHashes: %s mNewURIHasRef: %s", + this, !aLoadState->LoadIsFromSessionHistory() ? "true" : "false", + !aLoadState->PostDataStream() ? "true" : "false", + aState.mSameExceptHashes ? "true" : "false", + aState.mNewURIHasRef ? "true" : "false")); + return !aLoadState->LoadIsFromSessionHistory() && + !aLoadState->PostDataStream() && aState.mSameExceptHashes && + aState.mNewURIHasRef; +} + +nsresult nsDocShell::HandleSameDocumentNavigation( + nsDocShellLoadState* aLoadState, SameDocumentNavigationState& aState) { +#ifdef DEBUG + SameDocumentNavigationState state; + MOZ_ASSERT(IsSameDocumentNavigation(aLoadState, state)); +#endif + + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell::HandleSameDocumentNavigation %p %s -> %s", this, + mCurrentURI->GetSpecOrDefault().get(), + aLoadState->URI()->GetSpecOrDefault().get())); + + RefPtr<Document> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + doc->DoNotifyPossibleTitleChange(); + + nsCOMPtr<nsIURI> currentURI = mCurrentURI; + + // We need to upgrade the new URI from http: to https: + nsCOMPtr<nsIURI> newURI = aLoadState->URI(); + if (aState.mSecureUpgradeURI) { + MOZ_TRY(NS_GetSecureUpgradedURI(aLoadState->URI(), getter_AddRefs(newURI))); + MOZ_LOG(gSHLog, LogLevel::Debug, + ("Upgraded URI to %s", newURI->GetSpecOrDefault().get())); + } + +#ifdef DEBUG + if (aState.mSameExceptHashes) { + bool sameExceptHashes = false; + currentURI->EqualsExceptRef(newURI, &sameExceptHashes); + MOZ_ASSERT(sameExceptHashes); + } +#endif + + // Save the position of the scrollers. + nsPoint scrollPos = GetCurScrollPos(); + + // Reset mLoadType to its original value once we exit this block, because this + // same document navigation might have started after a normal, network load, + // and we don't want to clobber its load type. See bug 737307. + AutoRestore<uint32_t> loadTypeResetter(mLoadType); + + // If a non-same-document-navigation (i.e., a network load) is pending, make + // this a replacement load, so that we don't add a SHEntry here and the + // network load goes into the SHEntry it expects to. + if (JustStartedNetworkLoad() && (aLoadState->LoadType() & LOAD_CMD_NORMAL)) { + mLoadType = LOAD_NORMAL_REPLACE; + } else { + mLoadType = aLoadState->LoadType(); + } + + mURIResultedInDocument = true; + + nsCOMPtr<nsISHEntry> oldLSHE = mLSHE; + + // we need to assign aLoadState->SHEntry() to mLSHE right here, so that on + // History loads, SetCurrentURI() called from OnNewURI() will send proper + // onLocationChange() notifications to the browser to update back/forward + // buttons. + SetHistoryEntryAndUpdateBC(Some<nsISHEntry*>(aLoadState->SHEntry()), + Nothing()); + UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> oldLoadingEntry; + mLoadingEntry.swap(oldLoadingEntry); + if (aLoadState->GetLoadingSessionHistoryInfo()) { + mLoadingEntry = MakeUnique<LoadingSessionHistoryInfo>( + *aLoadState->GetLoadingSessionHistoryInfo()); + mNeedToReportActiveAfterLoadingBecomesActive = false; + } + + // Set the doc's URI according to the new history entry's URI. + doc->SetDocumentURI(newURI); + + /* This is a anchor traversal within the same page. + * call OnNewURI() so that, this traversal will be + * recorded in session and global history. + */ + nsCOMPtr<nsIPrincipal> newURITriggeringPrincipal, newURIPrincipalToInherit, + newURIPartitionedPrincipalToInherit; + nsCOMPtr<nsIContentSecurityPolicy> newCsp; + if (mozilla::SessionHistoryInParent() ? !!mActiveEntry : !!mOSHE) { + if (mozilla::SessionHistoryInParent()) { + newURITriggeringPrincipal = mActiveEntry->GetTriggeringPrincipal(); + newURIPrincipalToInherit = mActiveEntry->GetPrincipalToInherit(); + newURIPartitionedPrincipalToInherit = + mActiveEntry->GetPartitionedPrincipalToInherit(); + newCsp = mActiveEntry->GetCsp(); + } else { + newURITriggeringPrincipal = mOSHE->GetTriggeringPrincipal(); + newURIPrincipalToInherit = mOSHE->GetPrincipalToInherit(); + newURIPartitionedPrincipalToInherit = + mOSHE->GetPartitionedPrincipalToInherit(); + newCsp = mOSHE->GetCsp(); + } + } else { + newURITriggeringPrincipal = aLoadState->TriggeringPrincipal(); + newURIPrincipalToInherit = doc->NodePrincipal(); + newURIPartitionedPrincipalToInherit = doc->PartitionedPrincipal(); + newCsp = doc->GetCsp(); + } + + uint32_t locationChangeFlags = GetSameDocumentNavigationFlags(newURI); + + // Pass true for aCloneSHChildren, since we're not + // changing documents here, so all of our subframes are + // still relevant to the new session history entry. + // + // It also makes OnNewURI(...) set LOCATION_CHANGE_SAME_DOCUMENT + // flag on firing onLocationChange(...). + // Anyway, aCloneSHChildren param is simply reflecting + // doSameDocumentNavigation in this scope. + // + // Note: we'll actually fire onLocationChange later, in order to preserve + // ordering of HistoryCommit() in the parent vs onLocationChange (bug + // 1668126) + bool locationChangeNeeded = OnNewURI( + newURI, nullptr, newURITriggeringPrincipal, newURIPrincipalToInherit, + newURIPartitionedPrincipalToInherit, newCsp, false, true, true); + + nsCOMPtr<nsIInputStream> postData; + uint32_t cacheKey = 0; + + bool scrollRestorationIsManual = false; + if (!mozilla::SessionHistoryInParent()) { + if (mOSHE) { + /* save current position of scroller(s) (bug 59774) */ + mOSHE->SetScrollPosition(scrollPos.x, scrollPos.y); + scrollRestorationIsManual = mOSHE->GetScrollRestorationIsManual(); + // Get the postdata and page ident from the current page, if + // the new load is being done via normal means. Note that + // "normal means" can be checked for just by checking for + // LOAD_CMD_NORMAL, given the loadType and allowScroll check + // above -- it filters out some LOAD_CMD_NORMAL cases that we + // wouldn't want here. + if (aLoadState->LoadType() & LOAD_CMD_NORMAL) { + postData = mOSHE->GetPostData(); + cacheKey = mOSHE->GetCacheKey(); + } + + // Link our new SHEntry to the old SHEntry's back/forward + // cache data, since the two SHEntries correspond to the + // same document. + if (mLSHE) { + if (!aLoadState->LoadIsFromSessionHistory()) { + // If we're not doing a history load, scroll restoration + // should be inherited from the previous session history entry. + SetScrollRestorationIsManualOnHistoryEntry(mLSHE, + scrollRestorationIsManual); + } + mLSHE->AdoptBFCacheEntry(mOSHE); + } + } + } else { + if (mActiveEntry) { + mActiveEntry->SetScrollPosition(scrollPos.x, scrollPos.y); + if (mBrowsingContext) { + CollectWireframe(); + if (XRE_IsParentProcess()) { + SessionHistoryEntry* entry = + mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry(); + if (entry) { + entry->SetScrollPosition(scrollPos.x, scrollPos.y); + } + } else { + mozilla::Unused << ContentChild::GetSingleton() + ->SendSessionHistoryEntryScrollPosition( + mBrowsingContext, scrollPos.x, + scrollPos.y); + } + } + } + if (mLoadingEntry) { + if (!mLoadingEntry->mLoadIsFromSessionHistory) { + // If we're not doing a history load, scroll restoration + // should be inherited from the previous session history entry. + // XXX This needs most probably tweaks once fragment navigation is + // fixed to work with session-history-in-parent. + SetScrollRestorationIsManualOnHistoryEntry(nullptr, + scrollRestorationIsManual); + } + } + } + + // If we're doing a history load, use its scroll restoration state. + if (aLoadState->LoadIsFromSessionHistory()) { + if (mozilla::SessionHistoryInParent()) { + scrollRestorationIsManual = aLoadState->GetLoadingSessionHistoryInfo() + ->mInfo.GetScrollRestorationIsManual(); + } else { + scrollRestorationIsManual = + aLoadState->SHEntry()->GetScrollRestorationIsManual(); + } + } + + /* Assign mLSHE to mOSHE. This will either be a new entry created + * by OnNewURI() for normal loads or aLoadState->SHEntry() for history + * loads. + */ + if (!mozilla::SessionHistoryInParent()) { + if (mLSHE) { + SetHistoryEntryAndUpdateBC(Nothing(), Some<nsISHEntry*>(mLSHE)); + // Save the postData obtained from the previous page + // in to the session history entry created for the + // anchor page, so that any history load of the anchor + // page will restore the appropriate postData. + if (postData) { + mOSHE->SetPostData(postData); + } + + // Make sure we won't just repost without hitting the + // cache first + if (cacheKey != 0) { + mOSHE->SetCacheKey(cacheKey); + } + } + + /* Set the title for the SH entry for this target url so that + * SH menus in go/back/forward buttons won't be empty for this. + * Note, this happens on mOSHE (and mActiveEntry in the future) because of + * the code above. + * Note, when session history lives in the parent process, this does not + * update the title there. + */ + SetTitleOnHistoryEntry(false); + } else { + if (aLoadState->LoadIsFromSessionHistory()) { + MOZ_LOG( + gSHLog, LogLevel::Debug, + ("Moving the loading entry to the active entry on nsDocShell %p to " + "%s", + this, mLoadingEntry->mInfo.GetURI()->GetSpecOrDefault().get())); + + nsCOMPtr<nsILayoutHistoryState> currentLayoutHistoryState; + if (mActiveEntry) { + currentLayoutHistoryState = mActiveEntry->GetLayoutHistoryState(); + } + + UniquePtr<SessionHistoryInfo> previousActiveEntry(mActiveEntry.release()); + mActiveEntry = MakeUnique<SessionHistoryInfo>(mLoadingEntry->mInfo); + if (currentLayoutHistoryState) { + // Restore the existing nsILayoutHistoryState object, since it is + // possibly being used by the layout. When doing a new load, the + // shared state is copied from the existing active entry, so this + // special case is needed only with the history loads. + mActiveEntry->SetLayoutHistoryState(currentLayoutHistoryState); + } + + if (cacheKey != 0) { + mActiveEntry->SetCacheKey(cacheKey); + } + // We're passing in mCurrentURI, which could be null. SessionHistoryCommit + // does require a non-null uri if this is for a refresh load of the same + // URI, but in that case mCurrentURI won't be null here. + mBrowsingContext->SessionHistoryCommit( + *mLoadingEntry, mLoadType, mCurrentURI, previousActiveEntry.get(), + true, true, + /* No expiration update on the same document loads*/ + false, cacheKey); + // FIXME Need to set postdata. + + // Set the title for the SH entry for this target url so that + // SH menus in go/back/forward buttons won't be empty for this. + // Note, when session history lives in the parent process, this does not + // update the title there. + SetTitleOnHistoryEntry(false); + } else { + Maybe<bool> scrollRestorationIsManual; + if (mActiveEntry) { + scrollRestorationIsManual.emplace( + mActiveEntry->GetScrollRestorationIsManual()); + + // Get the postdata and page ident from the current page, if the new + // load is being done via normal means. Note that "normal means" can be + // checked for just by checking for LOAD_CMD_NORMAL, given the loadType + // and allowScroll check above -- it filters out some LOAD_CMD_NORMAL + // cases that we wouldn't want here. + if (aLoadState->LoadType() & LOAD_CMD_NORMAL) { + postData = mActiveEntry->GetPostData(); + cacheKey = mActiveEntry->GetCacheKey(); + } + } + + MOZ_LOG(gSHLog, LogLevel::Debug, + ("Creating an active entry on nsDocShell %p to %s", this, + newURI->GetSpecOrDefault().get())); + if (mActiveEntry) { + mActiveEntry = MakeUnique<SessionHistoryInfo>(*mActiveEntry, newURI); + } else { + mActiveEntry = MakeUnique<SessionHistoryInfo>( + newURI, newURITriggeringPrincipal, newURIPrincipalToInherit, + newURIPartitionedPrincipalToInherit, newCsp, mContentTypeHint); + } + + // Save the postData obtained from the previous page in to the session + // history entry created for the anchor page, so that any history load of + // the anchor page will restore the appropriate postData. + if (postData) { + mActiveEntry->SetPostData(postData); + } + + // Make sure we won't just repost without hitting the + // cache first + if (cacheKey != 0) { + mActiveEntry->SetCacheKey(cacheKey); + } + + // Set the title for the SH entry for this target url so that + // SH menus in go/back/forward buttons won't be empty for this. + mActiveEntry->SetTitle(mTitle); + + if (scrollRestorationIsManual.isSome()) { + mActiveEntry->SetScrollRestorationIsManual( + scrollRestorationIsManual.value()); + } + + if (LOAD_TYPE_HAS_FLAGS(mLoadType, LOAD_FLAGS_REPLACE_HISTORY)) { + mBrowsingContext->ReplaceActiveSessionHistoryEntry(mActiveEntry.get()); + } else { + mBrowsingContext->IncrementHistoryEntryCountForBrowsingContext(); + // FIXME We should probably just compute mChildOffset in the parent + // instead of passing it over IPC here. + mBrowsingContext->SetActiveSessionHistoryEntry( + Some(scrollPos), mActiveEntry.get(), mLoadType, cacheKey); + // FIXME Do we need to update mPreviousEntryIndex and mLoadedEntryIndex? + } + } + } + + if (locationChangeNeeded) { + FireOnLocationChange(this, nullptr, newURI, locationChangeFlags); + } + + /* Restore the original LSHE if we were loading something + * while same document navigation was initiated. + */ + SetHistoryEntryAndUpdateBC(Some<nsISHEntry*>(oldLSHE), Nothing()); + mLoadingEntry.swap(oldLoadingEntry); + + /* Set the title for the Global History entry for this anchor url. + */ + UpdateGlobalHistoryTitle(newURI); + + SetDocCurrentStateObj(mOSHE, mActiveEntry.get()); + + // Inform the favicon service that the favicon for oldURI also + // applies to newURI. + CopyFavicon(currentURI, newURI, UsePrivateBrowsing()); + + RefPtr<nsGlobalWindowOuter> scriptGlobal = mScriptGlobal; + RefPtr<nsGlobalWindowInner> win = + scriptGlobal ? scriptGlobal->GetCurrentInnerWindowInternal() : nullptr; + + // ScrollToAnchor doesn't necessarily cause us to scroll the window; + // the function decides whether a scroll is appropriate based on the + // arguments it receives. But even if we don't end up scrolling, + // ScrollToAnchor performs other important tasks, such as informing + // the presShell that we have a new hash. See bug 680257. + nsresult rv = ScrollToAnchor(aState.mCurrentURIHasRef, aState.mNewURIHasRef, + aState.mNewHash, aLoadState->LoadType()); + NS_ENSURE_SUCCESS(rv, rv); + + /* restore previous position of scroller(s), if we're moving + * back in history (bug 59774) + */ + nscoord bx = 0; + nscoord by = 0; + bool needsScrollPosUpdate = false; + if ((mozilla::SessionHistoryInParent() ? !!mActiveEntry : !!mOSHE) && + (aLoadState->LoadType() == LOAD_HISTORY || + aLoadState->LoadType() == LOAD_RELOAD_NORMAL) && + !scrollRestorationIsManual) { + needsScrollPosUpdate = true; + if (mozilla::SessionHistoryInParent()) { + mActiveEntry->GetScrollPosition(&bx, &by); + } else { + mOSHE->GetScrollPosition(&bx, &by); + } + } + + // Dispatch the popstate and hashchange events, as appropriate. + // + // The event dispatch below can cause us to re-enter script and + // destroy the docshell, nulling out mScriptGlobal. Hold a stack + // reference to avoid null derefs. See bug 914521. + if (win) { + // Fire a hashchange event URIs differ, and only in their hashes. + bool doHashchange = aState.mSameExceptHashes && + (aState.mCurrentURIHasRef != aState.mNewURIHasRef || + !aState.mCurrentHash.Equals(aState.mNewHash)); + + if (aState.mHistoryNavBetweenSameDoc || doHashchange) { + win->DispatchSyncPopState(); + } + + if (needsScrollPosUpdate && win->HasActiveDocument()) { + SetCurScrollPosEx(bx, by); + } + + if (doHashchange) { + // Note that currentURI hasn't changed because it's on the + // stack, so we can just use it directly as the old URI. + win->DispatchAsyncHashchange(currentURI, newURI); + } + } + + return NS_OK; +} + +static bool NavigationShouldTakeFocus(nsDocShell* aDocShell, + nsDocShellLoadState* aLoadState) { + if (!aLoadState->AllowFocusMove()) { + return false; + } + if (!aLoadState->HasValidUserGestureActivation()) { + return false; + } + const auto& sourceBC = aLoadState->SourceBrowsingContext(); + if (!sourceBC || !sourceBC->IsActive()) { + // If the navigation didn't come from a foreground tab, then we don't steal + // focus. + return false; + } + auto* bc = aDocShell->GetBrowsingContext(); + if (sourceBC.get() == bc) { + // If it comes from the same tab / frame, don't steal focus either. + return false; + } + auto* fm = nsFocusManager::GetFocusManager(); + if (fm && bc->IsActive() && fm->IsInActiveWindow(bc)) { + // If we're already on the foreground tab of the foreground window, then we + // don't need to do this. This helps to e.g. not steal focus from the + // browser chrome unnecessarily. + return false; + } + if (auto* doc = aDocShell->GetExtantDocument()) { + if (doc->IsInitialDocument()) { + // If we're the initial load for the browsing context, the browser + // chrome determines what to focus. This is important because the + // browser chrome may want to e.g focus the url-bar + return false; + } + } + // Take loadDivertedInBackground into account so the behavior would be the + // same as how the tab first opened. + return !Preferences::GetBool("browser.tabs.loadDivertedInBackground", false); +} + +nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState, + Maybe<uint32_t> aCacheKey) { + MOZ_ASSERT(aLoadState, "need a load state!"); + MOZ_ASSERT(aLoadState->TriggeringPrincipal(), + "need a valid TriggeringPrincipal"); + + if (!aLoadState->TriggeringPrincipal()) { + MOZ_ASSERT(false, "InternalLoad needs a valid triggeringPrincipal"); + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(mBrowsingContext->GetPendingInitialization())) { + return NS_ERROR_NOT_AVAILABLE; + } + + const bool shouldTakeFocus = NavigationShouldTakeFocus(this, aLoadState); + + mOriginalUriString.Truncate(); + + MOZ_LOG(gDocShellLeakLog, LogLevel::Debug, + ("DOCSHELL %p InternalLoad %s\n", this, + aLoadState->URI()->GetSpecOrDefault().get())); + + NS_ENSURE_TRUE(IsValidLoadType(aLoadState->LoadType()), NS_ERROR_INVALID_ARG); + + // Cancel loads coming from Docshells that are being destroyed. + if (mIsBeingDestroyed) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = EnsureScriptEnvironment(); + if (NS_FAILED(rv)) { + return rv; + } + + // If we have a target to move to, do that now. + if (!aLoadState->Target().IsEmpty()) { + return PerformRetargeting(aLoadState); + } + + if (aLoadState->TargetBrowsingContext().IsNull()) { + aLoadState->SetTargetBrowsingContext(GetBrowsingContext()); + } + + MOZ_DIAGNOSTIC_ASSERT( + aLoadState->TargetBrowsingContext() == GetBrowsingContext(), + "Load must be targeting this BrowsingContext"); + + MOZ_TRY(CheckDisallowedJavascriptLoad(aLoadState)); + + // If we don't have a target, we're loading into ourselves, and our load + // delegate may want to intercept that load. + SameDocumentNavigationState sameDocumentNavigationState; + bool sameDocument = + IsSameDocumentNavigation(aLoadState, sameDocumentNavigationState) && + !aLoadState->GetPendingRedirectedChannel(); + + // Note: We do this check both here and in BrowsingContext:: + // LoadURI/InternalLoad, since document-specific sandbox flags are only + // available in the process triggering the load, and we don't want the target + // process to have to trust the triggering process to do the appropriate + // checks for the BrowsingContext's sandbox flags. + MOZ_TRY(mBrowsingContext->CheckSandboxFlags(aLoadState)); + + NS_ENSURE_STATE(!HasUnloadedParent()); + + rv = CheckLoadingPermissions(); + if (NS_FAILED(rv)) { + return rv; + } + + if (mFiredUnloadEvent) { + if (IsOKToLoadURI(aLoadState->URI())) { + MOZ_ASSERT(aLoadState->Target().IsEmpty(), + "Shouldn't have a window target here!"); + + // If this is a replace load, make whatever load triggered + // the unload event also a replace load, so we don't + // create extra history entries. + if (LOAD_TYPE_HAS_FLAGS(aLoadState->LoadType(), + LOAD_FLAGS_REPLACE_HISTORY)) { + mLoadType = LOAD_NORMAL_REPLACE; + } + + // Do this asynchronously + nsCOMPtr<nsIRunnable> ev = new InternalLoadEvent(this, aLoadState); + return Dispatch(TaskCategory::Other, ev.forget()); + } + + // Just ignore this load attempt + return NS_OK; + } + + // If we are loading a URI that should inherit a security context (basically + // javascript: at this point), and the caller has said that principal + // inheritance is allowed, there are a few possible cases: + // + // 1) We are provided with the principal to inherit. In that case, we just use + // it. + // + // 2) The load is coming from some other application. In this case we don't + // want to inherit from whatever document we have loaded now, since the + // load is unrelated to it. + // + // 3) It's a load from our application, but does not provide an explicit + // principal to inherit. In that case, we want to inherit the principal of + // our current document, or of our parent document (if any) if we don't + // have a current document. + { + bool inherits; + + if (!aLoadState->HasLoadFlags(LOAD_FLAGS_FROM_EXTERNAL) && + !aLoadState->PrincipalToInherit() && + (aLoadState->HasInternalLoadFlags( + INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL)) && + NS_SUCCEEDED(nsContentUtils::URIInheritsSecurityContext( + aLoadState->URI(), &inherits)) && + inherits) { + aLoadState->SetPrincipalToInherit(GetInheritedPrincipal(true)); + } + // If principalToInherit is still null (e.g. if some of the conditions of + // were not satisfied), then no inheritance of any sort will happen: the + // load will just get a principal based on the URI being loaded. + } + + // If this docshell is owned by a frameloader, make sure to cancel + // possible frameloader initialization before loading a new page. + nsCOMPtr<nsIDocShellTreeItem> parent = GetInProcessParentDocshell(); + if (parent) { + RefPtr<Document> doc = parent->GetDocument(); + if (doc) { + doc->TryCancelFrameLoaderInitialization(this); + } + } + + // Before going any further vet loads initiated by external programs. + if (aLoadState->HasLoadFlags(LOAD_FLAGS_FROM_EXTERNAL)) { + MOZ_DIAGNOSTIC_ASSERT(aLoadState->LoadType() == LOAD_NORMAL); + + // Disallow external chrome: loads targetted at content windows + if (SchemeIsChrome(aLoadState->URI())) { + NS_WARNING("blocked external chrome: url -- use '--chrome' option"); + return NS_ERROR_FAILURE; + } + + // clear the decks to prevent context bleed-through (bug 298255) + rv = CreateAboutBlankContentViewer(nullptr, nullptr, nullptr, nullptr, + /* aIsInitialDocument */ false); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + } + + mAllowKeywordFixup = aLoadState->HasInternalLoadFlags( + INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP); + mURIResultedInDocument = false; // reset the clock... + + // See if this is actually a load between two history entries for the same + // document. If the process fails, or if we successfully navigate within the + // same document, return. + if (sameDocument) { + nsresult rv = + HandleSameDocumentNavigation(aLoadState, sameDocumentNavigationState); + NS_ENSURE_SUCCESS(rv, rv); + if (shouldTakeFocus) { + mBrowsingContext->Focus(CallerType::System, IgnoreErrors()); + } + return rv; + } + + // mContentViewer->PermitUnload can destroy |this| docShell, which + // causes the next call of CanSavePresentation to crash. + // Hold onto |this| until we return, to prevent a crash from happening. + // (bug#331040) + nsCOMPtr<nsIDocShell> kungFuDeathGrip(this); + + // Don't init timing for javascript:, since it generally doesn't + // actually start a load or anything. If it does, we'll init + // timing then, from OnStateChange. + + // XXXbz mTiming should know what channel it's for, so we don't + // need this hackery. + bool toBeReset = false; + bool isJavaScript = SchemeIsJavascript(aLoadState->URI()); + + if (!isJavaScript) { + toBeReset = MaybeInitTiming(); + } + bool isNotDownload = aLoadState->FileName().IsVoid(); + if (mTiming && isNotDownload) { + mTiming->NotifyBeforeUnload(); + } + // Check if the page doesn't want to be unloaded. The javascript: + // protocol handler deals with this for javascript: URLs. + if (!isJavaScript && isNotDownload && + !aLoadState->NotifiedBeforeUnloadListeners() && mContentViewer) { + bool okToUnload; + + // Check if request is exempted from HTTPSOnlyMode and if https-first is + // enabled, if so it means: + // * https-first failed to upgrade request to https + // * we already asked for permission to unload and the user accepted + // otherwise we wouldn't be here. + bool isPrivateWin = GetOriginAttributes().mPrivateBrowsingId > 0; + bool isHistoryOrReload = false; + uint32_t loadType = aLoadState->LoadType(); + + // Check if request is a reload. + if (loadType == LOAD_RELOAD_NORMAL || + loadType == LOAD_RELOAD_BYPASS_CACHE || + loadType == LOAD_RELOAD_BYPASS_PROXY || + loadType == LOAD_RELOAD_BYPASS_PROXY_AND_CACHE || + loadType == LOAD_HISTORY) { + isHistoryOrReload = true; + } + + // If it isn't a reload, the request already failed to be upgraded and + // https-first is enabled then don't ask the user again for permission to + // unload and just unload. + if (!isHistoryOrReload && aLoadState->IsExemptFromHTTPSOnlyMode() && + nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) { + rv = mContentViewer->PermitUnload( + nsIContentViewer::PermitUnloadAction::eDontPromptAndUnload, + &okToUnload); + } else { + rv = mContentViewer->PermitUnload(&okToUnload); + } + + if (NS_SUCCEEDED(rv) && !okToUnload) { + // The user chose not to unload the page, interrupt the + // load. + MaybeResetInitTiming(toBeReset); + return NS_OK; + } + } + + if (mTiming && isNotDownload) { + mTiming->NotifyUnloadAccepted(mCurrentURI); + } + + // In e10s, in the parent process, we refuse to load anything other than + // "safe" resources that we ship or trust enough to give "special" URLs. + // Similar check will be performed by the ParentProcessDocumentChannel if in + // use. + if (XRE_IsE10sParentProcess() && + !DocumentChannel::CanUseDocumentChannel(aLoadState->URI()) && + !CanLoadInParentProcess(aLoadState->URI())) { + return NS_ERROR_FAILURE; + } + + // Whenever a top-level browsing context is navigated, the user agent MUST + // lock the orientation of the document to the document's default + // orientation. We don't explicitly check for a top-level browsing context + // here because orientation is only set on top-level browsing contexts. + if (mBrowsingContext->GetOrientationLock() != hal::ScreenOrientation::None) { + MOZ_ASSERT(mBrowsingContext->IsTop()); + MOZ_ALWAYS_SUCCEEDS( + mBrowsingContext->SetOrientationLock(hal::ScreenOrientation::None)); + if (mBrowsingContext->IsActive()) { + ScreenOrientation::UpdateActiveOrientationLock( + hal::ScreenOrientation::None); + } + } + + // Check for saving the presentation here, before calling Stop(). + // This is necessary so that we can catch any pending requests. + // Since the new request has not been created yet, we pass null for the + // new request parameter. + // Also pass nullptr for the document, since it doesn't affect the return + // value for our purposes here. + bool savePresentation = + CanSavePresentation(aLoadState->LoadType(), nullptr, nullptr, + /* aReportBFCacheComboTelemetry */ true); + + // nsDocShell::CanSavePresentation is for non-SHIP version only. Do a + // separate check for SHIP so that we know if there are ongoing requests + // before calling Stop() below. + if (mozilla::SessionHistoryInParent()) { + Document* document = GetDocument(); + uint32_t flags = 0; + if (document && !document->CanSavePresentation(nullptr, flags, true)) { + // This forces some flags into the WindowGlobalParent's mBFCacheStatus, + // which we'll then use in CanonicalBrowsingContext::AllowedInBFCache, + // and in particular we'll store BFCacheStatus::REQUEST if needed. + // Also, we want to report all the flags to the parent process here (and + // not just BFCacheStatus::NOT_ALLOWED), so that it can update the + // telemetry data correctly. + document->DisallowBFCaching(flags); + } + } + + // Don't stop current network activity for javascript: URL's since + // they might not result in any data, and thus nothing should be + // stopped in those cases. In the case where they do result in + // data, the javascript: URL channel takes care of stopping + // current network activity. + if (!isJavaScript && isNotDownload) { + // Stop any current network activity. + // Also stop content if this is a zombie doc. otherwise + // the onload will be delayed by other loads initiated in the + // background by the first document that + // didn't fully load before the next load was initiated. + // If not a zombie, don't stop content until data + // starts arriving from the new URI... + + if ((mContentViewer && mContentViewer->GetPreviousViewer()) || + LOAD_TYPE_HAS_FLAGS(aLoadState->LoadType(), LOAD_FLAGS_STOP_CONTENT)) { + rv = Stop(nsIWebNavigation::STOP_ALL); + } else { + rv = Stop(nsIWebNavigation::STOP_NETWORK); + } + + if (NS_FAILED(rv)) { + return rv; + } + } + + mLoadType = aLoadState->LoadType(); + + // aLoadState->SHEntry() should be assigned to mLSHE, only after Stop() has + // been called. But when loading an error page, do not clear the + // mLSHE for the real page. + if (mLoadType != LOAD_ERROR_PAGE) { + SetHistoryEntryAndUpdateBC(Some<nsISHEntry*>(aLoadState->SHEntry()), + Nothing()); + if (aLoadState->LoadIsFromSessionHistory() && + !mozilla::SessionHistoryInParent()) { + // We're making history navigation or a reload. Make sure our history ID + // points to the same ID as SHEntry's docshell ID. + nsID historyID = {}; + aLoadState->SHEntry()->GetDocshellID(historyID); + + Unused << mBrowsingContext->SetHistoryID(historyID); + } + } + + mSavingOldViewer = savePresentation; + + // If we have a saved content viewer in history, restore and show it now. + if (aLoadState->LoadIsFromSessionHistory() && + (mLoadType & LOAD_CMD_HISTORY)) { + // https://html.spec.whatwg.org/#history-traversal: + // To traverse the history + // "If entry has a different Document object than the current entry, then + // run the following substeps: Remove any tasks queued by the history + // traversal task source..." + // Same document object case was handled already above with + // HandleSameDocumentNavigation call. + RefPtr<ChildSHistory> shistory = GetRootSessionHistory(); + if (shistory) { + shistory->RemovePendingHistoryNavigations(); + } + if (!mozilla::SessionHistoryInParent()) { + // It's possible that the previous viewer of mContentViewer is the + // viewer that will end up in aLoadState->SHEntry() when it gets closed. + // If that's the case, we need to go ahead and force it into its shentry + // so we can restore it. + if (mContentViewer) { + nsCOMPtr<nsIContentViewer> prevViewer = + mContentViewer->GetPreviousViewer(); + if (prevViewer) { +#ifdef DEBUG + nsCOMPtr<nsIContentViewer> prevPrevViewer = + prevViewer->GetPreviousViewer(); + NS_ASSERTION(!prevPrevViewer, "Should never have viewer chain here"); +#endif + nsCOMPtr<nsISHEntry> viewerEntry; + prevViewer->GetHistoryEntry(getter_AddRefs(viewerEntry)); + if (viewerEntry == aLoadState->SHEntry()) { + // Make sure this viewer ends up in the right place + mContentViewer->SetPreviousViewer(nullptr); + prevViewer->Destroy(); + } + } + } + nsCOMPtr<nsISHEntry> oldEntry = mOSHE; + bool restoring; + rv = RestorePresentation(aLoadState->SHEntry(), &restoring); + if (restoring) { + Telemetry::Accumulate(Telemetry::BFCACHE_PAGE_RESTORED, true); + return rv; + } + Telemetry::Accumulate(Telemetry::BFCACHE_PAGE_RESTORED, false); + + // We failed to restore the presentation, so clean up. + // Both the old and new history entries could potentially be in + // an inconsistent state. + if (NS_FAILED(rv)) { + if (oldEntry) { + oldEntry->SyncPresentationState(); + } + + aLoadState->SHEntry()->SyncPresentationState(); + } + } + } + + bool isTopLevelDoc = mBrowsingContext->IsTopContent(); + + OriginAttributes attrs = GetOriginAttributes(); + attrs.SetFirstPartyDomain(isTopLevelDoc, aLoadState->URI()); + + PredictorLearn(aLoadState->URI(), nullptr, + nsINetworkPredictor::LEARN_LOAD_TOPLEVEL, attrs); + PredictorPredict(aLoadState->URI(), nullptr, + nsINetworkPredictor::PREDICT_LOAD, attrs, nullptr); + + nsCOMPtr<nsIRequest> req; + rv = DoURILoad(aLoadState, aCacheKey, getter_AddRefs(req)); + + if (NS_SUCCEEDED(rv)) { + if (shouldTakeFocus) { + mBrowsingContext->Focus(CallerType::System, IgnoreErrors()); + } + } + + if (NS_FAILED(rv)) { + nsCOMPtr<nsIChannel> chan(do_QueryInterface(req)); + UnblockEmbedderLoadEventForFailure(); + nsCOMPtr<nsIURI> uri = aLoadState->URI(); + if (DisplayLoadError(rv, uri, nullptr, chan) && + // FIXME: At this point code was using internal load flags, but checking + // non-internal load flags? + aLoadState->HasLoadFlags(LOAD_FLAGS_ERROR_LOAD_CHANGES_RV)) { + return NS_ERROR_LOAD_SHOWED_ERRORPAGE; + } + + // We won't report any error if this is an unknown protocol error. The + // reason behind this is that it will allow enumeration of external + // protocols if we report an error for each unknown protocol. + if (NS_ERROR_UNKNOWN_PROTOCOL == rv) { + return NS_OK; + } + } + + return rv; +} + +/* static */ +bool nsDocShell::CanLoadInParentProcess(nsIURI* aURI) { + nsCOMPtr<nsIURI> uri = aURI; + // In e10s, in the parent process, we refuse to load anything other than + // "safe" resources that we ship or trust enough to give "special" URLs. + bool canLoadInParent = false; + if (NS_SUCCEEDED(NS_URIChainHasFlags( + uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, &canLoadInParent)) && + canLoadInParent) { + // We allow UI resources. + return true; + } + // For about: and extension-based URIs, which don't get + // URI_IS_UI_RESOURCE, first remove layers of view-source:, if present. + while (uri && uri->SchemeIs("view-source")) { + nsCOMPtr<nsINestedURI> nested = do_QueryInterface(uri); + if (nested) { + nested->GetInnerURI(getter_AddRefs(uri)); + } else { + break; + } + } + // Allow about: URIs, and allow moz-extension ones if we're running + // extension content in the parent process. + if (!uri || uri->SchemeIs("about") || + (!StaticPrefs::extensions_webextensions_remote() && + uri->SchemeIs("moz-extension"))) { + return true; + } +#ifdef MOZ_THUNDERBIRD + if (uri->SchemeIs("imap") || uri->SchemeIs("mailbox") || + uri->SchemeIs("news") || uri->SchemeIs("nntp") || + uri->SchemeIs("snews")) { + return true; + } +#endif + nsAutoCString scheme; + uri->GetScheme(scheme); + // Allow ext+foo URIs (extension-registered custom protocols). See + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/protocol_handlers + if (StringBeginsWith(scheme, "ext+"_ns) && + !StaticPrefs::extensions_webextensions_remote()) { + return true; + } + // Final exception for some legacy automated tests: + if (xpc::IsInAutomation() && + StaticPrefs::security_allow_unsafe_parent_loads()) { + return true; + } + return false; +} + +nsIPrincipal* nsDocShell::GetInheritedPrincipal( + bool aConsiderCurrentDocument, bool aConsiderPartitionedPrincipal) { + RefPtr<Document> document; + bool inheritedFromCurrent = false; + + if (aConsiderCurrentDocument && mContentViewer) { + document = mContentViewer->GetDocument(); + inheritedFromCurrent = true; + } + + if (!document) { + nsCOMPtr<nsIDocShellTreeItem> parentItem; + GetInProcessSameTypeParent(getter_AddRefs(parentItem)); + if (parentItem) { + document = parentItem->GetDocument(); + } + } + + if (!document) { + if (!aConsiderCurrentDocument) { + return nullptr; + } + + // Make sure we end up with _something_ as the principal no matter + // what.If this fails, we'll just get a null docViewer and bail. + EnsureContentViewer(); + if (!mContentViewer) { + return nullptr; + } + document = mContentViewer->GetDocument(); + } + + //-- Get the document's principal + if (document) { + nsIPrincipal* docPrincipal = aConsiderPartitionedPrincipal + ? document->PartitionedPrincipal() + : document->NodePrincipal(); + + // Don't allow loads in typeContent docShells to inherit the system + // principal from existing documents. + if (inheritedFromCurrent && mItemType == typeContent && + docPrincipal->IsSystemPrincipal()) { + return nullptr; + } + + return docPrincipal; + } + + return nullptr; +} + +/* static */ nsresult nsDocShell::CreateRealChannelForDocument( + nsIChannel** aChannel, nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIInterfaceRequestor* aCallbacks, nsLoadFlags aLoadFlags, + const nsAString& aSrcdoc, nsIURI* aBaseURI) { + nsCOMPtr<nsIChannel> channel; + if (aSrcdoc.IsVoid()) { + MOZ_TRY(NS_NewChannelInternal(getter_AddRefs(channel), aURI, aLoadInfo, + nullptr, // PerformanceStorage + nullptr, // loadGroup + aCallbacks, aLoadFlags)); + + if (aBaseURI) { + nsCOMPtr<nsIViewSourceChannel> vsc = do_QueryInterface(channel); + if (vsc) { + MOZ_ALWAYS_SUCCEEDS(vsc->SetBaseURI(aBaseURI)); + } + } + } else if (SchemeIsViewSource(aURI)) { + // Instantiate view source handler protocol, if it doesn't exist already. + nsCOMPtr<nsIIOService> io(do_GetIOService()); + MOZ_ASSERT(io); + nsCOMPtr<nsIProtocolHandler> handler; + nsresult rv = + io->GetProtocolHandler("view-source", getter_AddRefs(handler)); + if (NS_FAILED(rv)) { + return rv; + } + + nsViewSourceHandler* vsh = nsViewSourceHandler::GetInstance(); + if (!vsh) { + return NS_ERROR_FAILURE; + } + + MOZ_TRY(vsh->NewSrcdocChannel(aURI, aBaseURI, aSrcdoc, aLoadInfo, + getter_AddRefs(channel))); + } else { + MOZ_TRY(NS_NewInputStreamChannelInternal(getter_AddRefs(channel), aURI, + aSrcdoc, "text/html"_ns, aLoadInfo, + true)); + nsCOMPtr<nsIInputStreamChannel> isc = do_QueryInterface(channel); + MOZ_ASSERT(isc); + isc->SetBaseURI(aBaseURI); + } + + if (aLoadFlags != nsIRequest::LOAD_NORMAL) { + nsresult rv = channel->SetLoadFlags(aLoadFlags); + NS_ENSURE_SUCCESS(rv, rv); + } + + channel.forget(aChannel); + return NS_OK; +} + +/* static */ bool nsDocShell::CreateAndConfigureRealChannelForLoadState( + BrowsingContext* aBrowsingContext, nsDocShellLoadState* aLoadState, + LoadInfo* aLoadInfo, nsIInterfaceRequestor* aCallbacks, + nsDocShell* aDocShell, const OriginAttributes& aOriginAttributes, + nsLoadFlags aLoadFlags, uint32_t aCacheKey, nsresult& aRv, + nsIChannel** aChannel) { + MOZ_ASSERT(aLoadInfo); + + nsString srcdoc = VoidString(); + bool isSrcdoc = + aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_IS_SRCDOC); + if (isSrcdoc) { + srcdoc = aLoadState->SrcdocData(); + } + + aLoadInfo->SetTriggeringRemoteType( + aLoadState->GetEffectiveTriggeringRemoteType()); + + if (aLoadState->PrincipalToInherit()) { + aLoadInfo->SetPrincipalToInherit(aLoadState->PrincipalToInherit()); + } + aLoadInfo->SetLoadTriggeredFromExternal( + aLoadState->HasLoadFlags(LOAD_FLAGS_FROM_EXTERNAL)); + aLoadInfo->SetForceAllowDataURI(aLoadState->HasInternalLoadFlags( + INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI)); + aLoadInfo->SetOriginalFrameSrcLoad( + aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_ORIGINAL_FRAME_SRC)); + + bool inheritAttrs = false; + if (aLoadState->PrincipalToInherit()) { + inheritAttrs = nsContentUtils::ChannelShouldInheritPrincipal( + aLoadState->PrincipalToInherit(), aLoadState->URI(), + true, // aInheritForAboutBlank + isSrcdoc); + } + + // Strip the target query parameters before creating the channel. + aLoadState->MaybeStripTrackerQueryStrings(aBrowsingContext); + + OriginAttributes attrs; + + // Inherit origin attributes from PrincipalToInherit if inheritAttrs is + // true. Otherwise we just use the origin attributes from docshell. + if (inheritAttrs) { + MOZ_ASSERT(aLoadState->PrincipalToInherit(), + "We should have PrincipalToInherit here."); + attrs = aLoadState->PrincipalToInherit()->OriginAttributesRef(); + // If firstPartyIsolation is not enabled, then PrincipalToInherit should + // have the same origin attributes with docshell. + MOZ_ASSERT_IF(!OriginAttributes::IsFirstPartyEnabled(), + attrs == aOriginAttributes); + } else { + attrs = aOriginAttributes; + attrs.SetFirstPartyDomain(IsTopLevelDoc(aBrowsingContext, aLoadInfo), + aLoadState->URI()); + } + + aRv = aLoadInfo->SetOriginAttributes(attrs); + if (NS_WARN_IF(NS_FAILED(aRv))) { + return false; + } + + if (aLoadState->GetIsFromProcessingFrameAttributes()) { + aLoadInfo->SetIsFromProcessingFrameAttributes(); + } + + // Propagate the IsFormSubmission flag to the loadInfo. + if (aLoadState->IsFormSubmission()) { + aLoadInfo->SetIsFormSubmission(true); + } + + aLoadInfo->SetUnstrippedURI(aLoadState->GetUnstrippedURI()); + + nsCOMPtr<nsIChannel> channel; + aRv = CreateRealChannelForDocument(getter_AddRefs(channel), aLoadState->URI(), + aLoadInfo, aCallbacks, aLoadFlags, srcdoc, + aLoadState->BaseURI()); + NS_ENSURE_SUCCESS(aRv, false); + + if (!channel) { + return false; + } + + // If the HTTPS-Only mode is enabled, every insecure request gets upgraded to + // HTTPS by default. This behavior can be disabled through the loadinfo flag + // HTTPS_ONLY_EXEMPT. + nsHTTPSOnlyUtils::TestSitePermissionAndPotentiallyAddExemption(channel); + + // hack + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel)); + nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal( + do_QueryInterface(channel)); + nsCOMPtr<nsIURI> referrer; + nsIReferrerInfo* referrerInfo = aLoadState->GetReferrerInfo(); + if (referrerInfo) { + referrerInfo->GetOriginalReferrer(getter_AddRefs(referrer)); + } + if (httpChannelInternal) { + if (aLoadState->HasInternalLoadFlags( + INTERNAL_LOAD_FLAGS_FORCE_ALLOW_COOKIES)) { + aRv = httpChannelInternal->SetThirdPartyFlags( + nsIHttpChannelInternal::THIRD_PARTY_FORCE_ALLOW); + MOZ_ASSERT(NS_SUCCEEDED(aRv)); + } + if (aLoadState->FirstParty()) { + aRv = httpChannelInternal->SetDocumentURI(aLoadState->URI()); + MOZ_ASSERT(NS_SUCCEEDED(aRv)); + } else { + aRv = httpChannelInternal->SetDocumentURI(referrer); + MOZ_ASSERT(NS_SUCCEEDED(aRv)); + } + aRv = httpChannelInternal->SetRedirectMode( + nsIHttpChannelInternal::REDIRECT_MODE_MANUAL); + MOZ_ASSERT(NS_SUCCEEDED(aRv)); + } + + if (httpChannel) { + if (aLoadState->HeadersStream()) { + aRv = AddHeadersToChannel(aLoadState->HeadersStream(), httpChannel); + } + // Set the referrer explicitly + // Referrer is currenly only set for link clicks here. + if (referrerInfo) { + aRv = httpChannel->SetReferrerInfo(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(aRv)); + } + + // Mark the http channel as UrgentStart for top level document loading in + // active tab. + if (IsUrgentStart(aBrowsingContext, aLoadInfo, aLoadState->LoadType())) { + nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel)); + if (cos) { + cos->AddClassFlags(nsIClassOfService::UrgentStart); + } + } + } + + channel->SetOriginalURI(aLoadState->OriginalURI() ? aLoadState->OriginalURI() + : aLoadState->URI()); + + const nsACString& typeHint = aLoadState->TypeHint(); + if (!typeHint.IsVoid()) { + channel->SetContentType(typeHint); + } + + const nsAString& fileName = aLoadState->FileName(); + if (!fileName.IsVoid()) { + aRv = channel->SetContentDisposition(nsIChannel::DISPOSITION_ATTACHMENT); + NS_ENSURE_SUCCESS(aRv, false); + if (!fileName.IsEmpty()) { + aRv = channel->SetContentDispositionFilename(fileName); + NS_ENSURE_SUCCESS(aRv, false); + } + } + + if (nsCOMPtr<nsIWritablePropertyBag2> props = do_QueryInterface(channel)) { + nsCOMPtr<nsIURI> referrer; + nsIReferrerInfo* referrerInfo = aLoadState->GetReferrerInfo(); + if (referrerInfo) { + referrerInfo->GetOriginalReferrer(getter_AddRefs(referrer)); + } + // save true referrer for those who need it (e.g. xpinstall whitelisting) + // Currently only http and ftp channels support this. + props->SetPropertyAsInterface(u"docshell.internalReferrer"_ns, referrer); + + if (aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_FIRST_LOAD)) { + props->SetPropertyAsBool(u"docshell.newWindowTarget"_ns, true); + } + } + + nsCOMPtr<nsICacheInfoChannel> cacheChannel(do_QueryInterface(channel)); + auto loadType = aLoadState->LoadType(); + + if (loadType == LOAD_RELOAD_NORMAL && + StaticPrefs:: + browser_soft_reload_only_force_validate_top_level_document()) { + nsCOMPtr<nsICacheInfoChannel> cachingChannel = do_QueryInterface(channel); + if (cachingChannel) { + cachingChannel->SetForceValidateCacheContent(true); + } + } + + // figure out if we need to set the post data stream on the channel... + if (aLoadState->PostDataStream()) { + if (nsCOMPtr<nsIFormPOSTActionChannel> postChannel = + do_QueryInterface(channel)) { + // XXX it's a bit of a hack to rewind the postdata stream here but + // it has to be done in case the post data is being reused multiple + // times. + nsCOMPtr<nsISeekableStream> postDataSeekable = + do_QueryInterface(aLoadState->PostDataStream()); + if (postDataSeekable) { + aRv = postDataSeekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + NS_ENSURE_SUCCESS(aRv, false); + } + + // we really need to have a content type associated with this stream!! + postChannel->SetUploadStream(aLoadState->PostDataStream(), ""_ns, -1); + + // Ownership of the stream has transferred to the channel, clear our + // reference. + aLoadState->SetPostDataStream(nullptr); + } + + /* If there is a valid postdata *and* it is a History Load, + * set up the cache key on the channel, to retrieve the + * data *only* from the cache. If it is a normal reload, the + * cache is free to go to the server for updated postdata. + */ + if (cacheChannel && aCacheKey != 0) { + if (loadType == LOAD_HISTORY || loadType == LOAD_RELOAD_CHARSET_CHANGE) { + cacheChannel->SetCacheKey(aCacheKey); + uint32_t loadFlags; + if (NS_SUCCEEDED(channel->GetLoadFlags(&loadFlags))) { + channel->SetLoadFlags(loadFlags | + nsICachingChannel::LOAD_ONLY_FROM_CACHE); + } + } else if (loadType == LOAD_RELOAD_NORMAL) { + cacheChannel->SetCacheKey(aCacheKey); + } + } + } else { + /* If there is no postdata, set the cache key on the channel, and + * do not set the LOAD_ONLY_FROM_CACHE flag, so that the channel + * will be free to get it from net if it is not found in cache. + * New cache may use it creatively on CGI pages with GET + * method and even on those that say "no-cache" + */ + if (loadType == LOAD_HISTORY || loadType == LOAD_RELOAD_NORMAL || + loadType == LOAD_RELOAD_CHARSET_CHANGE || + loadType == LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE || + loadType == LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE) { + if (cacheChannel && aCacheKey != 0) { + cacheChannel->SetCacheKey(aCacheKey); + } + } + } + + if (nsCOMPtr<nsIScriptChannel> scriptChannel = do_QueryInterface(channel)) { + // Allow execution against our context if the principals match + scriptChannel->SetExecutionPolicy(nsIScriptChannel::EXECUTE_NORMAL); + } + + if (nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(channel)) { + timedChannel->SetTimingEnabled(true); + + nsString initiatorType; + switch (aLoadInfo->InternalContentPolicyType()) { + case nsIContentPolicy::TYPE_INTERNAL_EMBED: + initiatorType = u"embed"_ns; + break; + case nsIContentPolicy::TYPE_INTERNAL_OBJECT: + initiatorType = u"object"_ns; + break; + default: { + const auto& embedderElementType = + aBrowsingContext->GetEmbedderElementType(); + if (embedderElementType) { + initiatorType = *embedderElementType; + } + break; + } + } + + if (!initiatorType.IsEmpty()) { + timedChannel->SetInitiatorType(initiatorType); + } + } + + nsCOMPtr<nsIURI> rpURI; + aLoadInfo->GetResultPrincipalURI(getter_AddRefs(rpURI)); + Maybe<nsCOMPtr<nsIURI>> originalResultPrincipalURI; + aLoadState->GetMaybeResultPrincipalURI(originalResultPrincipalURI); + if (originalResultPrincipalURI && + (!aLoadState->KeepResultPrincipalURIIfSet() || !rpURI)) { + // Unconditionally override, we want the replay to be equal to what has + // been captured. + aLoadInfo->SetResultPrincipalURI(originalResultPrincipalURI.ref()); + } + + if (aLoadState->OriginalURI() && aLoadState->LoadReplace()) { + // The LOAD_REPLACE flag and its handling here will be removed as part + // of bug 1319110. For now preserve its restoration here to not break + // any code expecting it being set specially on redirected channels. + // If the flag has originally been set to change result of + // NS_GetFinalChannelURI it won't have any effect and also won't cause + // any harm. + uint32_t loadFlags; + aRv = channel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(aRv, false); + channel->SetLoadFlags(loadFlags | nsIChannel::LOAD_REPLACE); + } + + nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadState->Csp(); + if (csp) { + // Navigational requests that are same origin need to be upgraded in case + // upgrade-insecure-requests is present. Please note that for document + // navigations that bit is re-computed in case we encounter a server + // side redirect so the navigation is not same-origin anymore. + bool upgradeInsecureRequests = false; + csp->GetUpgradeInsecureRequests(&upgradeInsecureRequests); + if (upgradeInsecureRequests) { + // only upgrade if the navigation is same origin + nsCOMPtr<nsIPrincipal> resultPrincipal; + aRv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + channel, getter_AddRefs(resultPrincipal)); + NS_ENSURE_SUCCESS(aRv, false); + if (nsContentSecurityUtils::IsConsideredSameOriginForUIR( + aLoadState->TriggeringPrincipal(), resultPrincipal)) { + aLoadInfo->SetUpgradeInsecureRequests(true); + } + } + + // For document loads we store the CSP that potentially needs to + // be inherited by the new document, e.g. in case we are loading + // an opaque origin like a data: URI. The actual inheritance + // check happens within Document::InitCSP(). + // Please create an actual copy of the CSP (do not share the same + // reference) otherwise a Meta CSP of an opaque origin will + // incorrectly be propagated to the embedding document. + RefPtr<nsCSPContext> cspToInherit = new nsCSPContext(); + cspToInherit->InitFromOther(static_cast<nsCSPContext*>(csp.get())); + aLoadInfo->SetCSPToInherit(cspToInherit); + } + + channel.forget(aChannel); + return true; +} + +bool nsDocShell::IsAboutBlankLoadOntoInitialAboutBlank( + nsIURI* aURI, bool aInheritPrincipal, nsIPrincipal* aPrincipalToInherit) { + return NS_IsAboutBlank(aURI) && aInheritPrincipal && + (aPrincipalToInherit == GetInheritedPrincipal(false)) && + (!mContentViewer || !mContentViewer->GetDocument() || + mContentViewer->GetDocument()->IsInitialDocument()); +} + +nsresult nsDocShell::DoURILoad(nsDocShellLoadState* aLoadState, + Maybe<uint32_t> aCacheKey, + nsIRequest** aRequest) { + // Double-check that we're still around to load this URI. + if (mIsBeingDestroyed) { + // Return NS_OK despite not doing anything to avoid throwing exceptions + // from nsLocation::SetHref if the unload handler of the existing page + // tears us down. + return NS_OK; + } + + nsCOMPtr<nsIURILoader> uriLoader = components::URILoader::Service(); + if (NS_WARN_IF(!uriLoader)) { + return NS_ERROR_UNEXPECTED; + } + + // Persist and sync layout history state before we load a new uri, as this + // might be our last chance to do so, in the content process. + PersistLayoutHistoryState(); + SynchronizeLayoutHistoryState(); + + nsresult rv; + nsContentPolicyType contentPolicyType = DetermineContentType(); + + if (IsSubframe()) { + MOZ_ASSERT(contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_IFRAME || + contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_FRAME, + "DoURILoad thinks this is a frame and InternalLoad does not"); + + if (StaticPrefs::dom_block_external_protocol_in_iframes()) { + // Only allow URLs able to return data in iframes. + if (nsContentUtils::IsExternalProtocol(aLoadState->URI())) { + // The context to check user-interaction with for the purposes of + // popup-blocking. + // + // We generally want to check the context that initiated the navigation. + WindowContext* sourceWindowContext = [&] { + const MaybeDiscardedBrowsingContext& sourceBC = + aLoadState->SourceBrowsingContext(); + if (!sourceBC.IsNullOrDiscarded()) { + if (WindowContext* wc = sourceBC.get()->GetCurrentWindowContext()) { + return wc; + } + } + return mBrowsingContext->GetParentWindowContext(); + }(); + + MOZ_ASSERT(sourceWindowContext); + // FIXME: We can't check user-interaction against an OOP window. This is + // the next best thing we can really do. The load state keeps whether + // the navigation had a user interaction in process + // (aLoadState->HasValidUserGestureActivation()), but we can't really + // consume it, which we want to prevent popup-spamming from the same + // click event. + WindowContext* context = + sourceWindowContext->IsInProcess() + ? sourceWindowContext + : mBrowsingContext->GetCurrentWindowContext(); + const bool popupBlocked = [&] { + const bool active = mBrowsingContext->IsActive(); + + // For same-origin-with-top windows, we grant a single free popup + // without user activation, see bug 1680721. + // + // We consume the flag now even if there's no user activation. + const bool hasFreePass = [&] { + if (!active || + !(context->IsInProcess() && context->SameOriginWithTop())) { + return false; + } + nsGlobalWindowInner* win = + context->TopWindowContext()->GetInnerWindow(); + return win && win->TryOpenExternalProtocolIframe(); + }(); + + if (context->IsInProcess() && + context->ConsumeTransientUserGestureActivation()) { + // If the user has interacted with the page, consume it. + return false; + } + + // TODO(emilio): Can we remove this check? It seems like what prompted + // this code (bug 1514547) should be covered by transient user + // activation, see bug 1514547. + if (active && + PopupBlocker::ConsumeTimerTokenForExternalProtocolIframe()) { + return false; + } + + if (sourceWindowContext->CanShowPopup()) { + return false; + } + + if (hasFreePass) { + return false; + } + + return true; + }(); + + // No error must be returned when iframes are blocked. + if (popupBlocked) { + nsAutoString message; + nsresult rv = nsContentUtils::GetLocalizedString( + nsContentUtils::eDOM_PROPERTIES, + "ExternalProtocolFrameBlockedNoUserActivation", message); + if (NS_SUCCEEDED(rv)) { + nsContentUtils::ReportToConsoleByWindowID( + message, nsIScriptError::warningFlag, "DOM"_ns, + context->InnerWindowId()); + } + return NS_OK; + } + } + } + + // Only allow view-source scheme in top-level docshells. view-source is + // the only scheme to which this applies at the moment due to potential + // timing attacks to read data from cross-origin iframes. If this widens + // we should add a protocol flag for whether the scheme is allowed in + // frames and use something like nsNetUtil::NS_URIChainHasFlags. + nsCOMPtr<nsIURI> tempURI = aLoadState->URI(); + nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(tempURI); + while (nestedURI) { + // view-source should always be an nsINestedURI, loop and check the + // scheme on this and all inner URIs that are also nested URIs. + if (SchemeIsViewSource(tempURI)) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + nestedURI->GetInnerURI(getter_AddRefs(tempURI)); + nestedURI = do_QueryInterface(tempURI); + } + } else { + MOZ_ASSERT(contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT, + "DoURILoad thinks this is a document and InternalLoad does not"); + } + + // We want to inherit aLoadState->PrincipalToInherit() when: + // 1. ChannelShouldInheritPrincipal returns true. + // 2. aLoadState->URI() is not data: URI, or data: URI is not + // configured as unique opaque origin. + bool inheritPrincipal = false; + + if (aLoadState->PrincipalToInherit()) { + bool isSrcdoc = + aLoadState->HasInternalLoadFlags(INTERNAL_LOAD_FLAGS_IS_SRCDOC); + bool inheritAttrs = nsContentUtils::ChannelShouldInheritPrincipal( + aLoadState->PrincipalToInherit(), aLoadState->URI(), + true, // aInheritForAboutBlank + isSrcdoc); + + inheritPrincipal = inheritAttrs && !SchemeIsData(aLoadState->URI()); + } + + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1736570 + const bool isAboutBlankLoadOntoInitialAboutBlank = + IsAboutBlankLoadOntoInitialAboutBlank(aLoadState->URI(), inheritPrincipal, + aLoadState->PrincipalToInherit()); + + // FIXME We still have a ton of codepaths that don't pass through + // DocumentLoadListener, so probably need to create session history info + // in more places. + if (aLoadState->GetLoadingSessionHistoryInfo()) { + SetLoadingSessionHistoryInfo(*aLoadState->GetLoadingSessionHistoryInfo()); + } else if (isAboutBlankLoadOntoInitialAboutBlank && + mozilla::SessionHistoryInParent()) { + // Materialize LoadingSessionHistoryInfo here, because DocumentChannel + // loads have it, and later history behavior depends on it existing. + UniquePtr<SessionHistoryInfo> entry = MakeUnique<SessionHistoryInfo>( + aLoadState->URI(), aLoadState->TriggeringPrincipal(), + aLoadState->PrincipalToInherit(), + aLoadState->PartitionedPrincipalToInherit(), aLoadState->Csp(), + mContentTypeHint); + mozilla::dom::LoadingSessionHistoryInfo info(*entry); + SetLoadingSessionHistoryInfo(info, true); + } + + // open a channel for the url + + // If we have a pending channel, use the channel we've already created here. + // We don't need to set up load flags for our channel, as it has already been + // created. + + if (nsCOMPtr<nsIChannel> channel = + aLoadState->GetPendingRedirectedChannel()) { + // If we have a request outparameter, shove our channel into it. + if (aRequest) { + nsCOMPtr<nsIRequest> outRequest = channel; + outRequest.forget(aRequest); + } + + return OpenRedirectedChannel(aLoadState); + } + + // There are two cases we care about: + // * Top-level load: In this case, loadingNode is null, but loadingWindow + // is our mScriptGlobal. We pass null for loadingPrincipal in this case. + // * Subframe load: loadingWindow is null, but loadingNode is the frame + // element for the load. loadingPrincipal is the NodePrincipal of the + // frame element. + nsCOMPtr<nsINode> loadingNode; + nsCOMPtr<nsPIDOMWindowOuter> loadingWindow; + nsCOMPtr<nsIPrincipal> loadingPrincipal; + nsCOMPtr<nsISupports> topLevelLoadingContext; + + if (contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT) { + loadingNode = nullptr; + loadingPrincipal = nullptr; + loadingWindow = mScriptGlobal; + if (XRE_IsContentProcess()) { + // In e10s the child process doesn't have access to the element that + // contains the browsing context (because that element is in the chrome + // process). + nsCOMPtr<nsIBrowserChild> browserChild = GetBrowserChild(); + topLevelLoadingContext = ToSupports(browserChild); + } else { + // This is for loading non-e10s tabs and toplevel windows of various + // sorts. + // For the toplevel window cases, requestingElement will be null. + nsCOMPtr<Element> requestingElement = + loadingWindow->GetFrameElementInternal(); + topLevelLoadingContext = requestingElement; + } + } else { + loadingWindow = nullptr; + loadingNode = mScriptGlobal->GetFrameElementInternal(); + if (loadingNode) { + // If we have a loading node, then use that as our loadingPrincipal. + loadingPrincipal = loadingNode->NodePrincipal(); +#ifdef DEBUG + // Get the docshell type for requestingElement. + RefPtr<Document> requestingDoc = loadingNode->OwnerDoc(); + nsCOMPtr<nsIDocShell> elementDocShell = requestingDoc->GetDocShell(); + // requestingElement docshell type = current docshell type. + MOZ_ASSERT( + mItemType == elementDocShell->ItemType(), + "subframes should have the same docshell type as their parent"); +#endif + } else { + if (mIsBeingDestroyed) { + // If this isn't a top-level load and mScriptGlobal's frame element is + // null, then the element got removed from the DOM while we were trying + // to load this resource. This docshell is scheduled for destruction + // already, so bail out here. + return NS_OK; + } + // If we are not being destroyed and we do not have access to the loading + // node, then we are a remote subframe. Set the loading principal + // to be a null principal and then set it correctly in the parent. + loadingPrincipal = NullPrincipal::Create(GetOriginAttributes(), nullptr); + } + } + + if (!aLoadState->TriggeringPrincipal()) { + MOZ_ASSERT(false, "DoURILoad needs a valid triggeringPrincipal"); + return NS_ERROR_FAILURE; + } + + uint32_t sandboxFlags = mBrowsingContext->GetSandboxFlags(); + nsSecurityFlags securityFlags = + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + + if (mLoadType == LOAD_ERROR_PAGE) { + securityFlags |= nsILoadInfo::SEC_LOAD_ERROR_PAGE; + } + + if (inheritPrincipal) { + securityFlags |= nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL; + } + + // Must never have a parent for TYPE_DOCUMENT loads + MOZ_ASSERT_IF(contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT, + !mBrowsingContext->GetParent()); + // Subdocuments must have a parent + MOZ_ASSERT_IF(contentPolicyType == nsIContentPolicy::TYPE_SUBDOCUMENT, + mBrowsingContext->GetParent()); + mBrowsingContext->SetTriggeringAndInheritPrincipals( + aLoadState->TriggeringPrincipal(), aLoadState->PrincipalToInherit(), + aLoadState->GetLoadIdentifier()); + RefPtr<LoadInfo> loadInfo = + (contentPolicyType == nsIContentPolicy::TYPE_DOCUMENT) + ? new LoadInfo(loadingWindow, aLoadState->URI(), + aLoadState->TriggeringPrincipal(), + topLevelLoadingContext, securityFlags, sandboxFlags) + : new LoadInfo(loadingPrincipal, aLoadState->TriggeringPrincipal(), + loadingNode, securityFlags, contentPolicyType, + Maybe<mozilla::dom::ClientInfo>(), + Maybe<mozilla::dom::ServiceWorkerDescriptor>(), + sandboxFlags); + RefPtr<WindowContext> context = mBrowsingContext->GetCurrentWindowContext(); + + if (isAboutBlankLoadOntoInitialAboutBlank) { + // Match the DocumentChannel case where the default for third-partiness + // differs from the default in LoadInfo construction here. + // toolkit/components/antitracking/test/browser/browser_aboutblank.js + // fails without this. + BrowsingContext* top = mBrowsingContext->Top(); + if (top == mBrowsingContext) { + // If we're at the top, this must be a window.open()ed + // window, and we can't be third-party relative to ourselves. + loadInfo->SetIsThirdPartyContextToTopWindow(false); + } else { + if (Document* topDoc = top->GetDocument()) { + bool thirdParty = false; + mozilla::Unused << topDoc->GetPrincipal()->IsThirdPartyPrincipal( + aLoadState->PrincipalToInherit(), &thirdParty); + loadInfo->SetIsThirdPartyContextToTopWindow(thirdParty); + } else { + // If top is in a different process, we have to be third-party relative + // to it. + loadInfo->SetIsThirdPartyContextToTopWindow(true); + } + } + } + + if (mLoadType != LOAD_ERROR_PAGE && context && context->IsInProcess() && + context->HasValidTransientUserGestureActivation()) { + aLoadState->SetHasValidUserGestureActivation(true); + } + + // in case this docshell load was triggered by a valid transient user gesture, + // or also the load originates from external, then we pass that information on + // to the loadinfo, which allows e.g. setting Sec-Fetch-User request headers. + if (aLoadState->HasValidUserGestureActivation() || + aLoadState->HasLoadFlags(LOAD_FLAGS_FROM_EXTERNAL)) { + loadInfo->SetHasValidUserGestureActivation(true); + } + loadInfo->SetTriggeringSandboxFlags(aLoadState->TriggeringSandboxFlags()); + loadInfo->SetIsMetaRefresh(aLoadState->IsMetaRefresh()); + + uint32_t cacheKey = 0; + if (aCacheKey) { + cacheKey = *aCacheKey; + } else if (mozilla::SessionHistoryInParent()) { + if (mLoadingEntry) { + cacheKey = mLoadingEntry->mInfo.GetCacheKey(); + } else if (mActiveEntry) { // for reload cases + cacheKey = mActiveEntry->GetCacheKey(); + } + } else { + if (mLSHE) { + cacheKey = mLSHE->GetCacheKey(); + } else if (mOSHE) { // for reload cases + cacheKey = mOSHE->GetCacheKey(); + } + } + + bool uriModified; + if (mLSHE || mLoadingEntry) { + if (mLoadingEntry) { + uriModified = mLoadingEntry->mInfo.GetURIWasModified(); + } else { + uriModified = mLSHE->GetURIWasModified(); + } + } else { + uriModified = false; + } + + bool isXFOError = false; + if (mFailedChannel) { + nsresult status; + mFailedChannel->GetStatus(&status); + isXFOError = status == NS_ERROR_XFO_VIOLATION; + } + + nsLoadFlags loadFlags = aLoadState->CalculateChannelLoadFlags( + mBrowsingContext, Some(uriModified), Some(isXFOError)); + + nsCOMPtr<nsIChannel> channel; + if (DocumentChannel::CanUseDocumentChannel(aLoadState->URI()) && + !isAboutBlankLoadOntoInitialAboutBlank) { + channel = DocumentChannel::CreateForDocument(aLoadState, loadInfo, + loadFlags, this, cacheKey, + uriModified, isXFOError); + MOZ_ASSERT(channel); + + // Disable keyword fixup when using DocumentChannel, since + // DocumentLoadListener will handle this for us (in the parent process). + mAllowKeywordFixup = false; + } else if (!CreateAndConfigureRealChannelForLoadState( + mBrowsingContext, aLoadState, loadInfo, this, this, + GetOriginAttributes(), loadFlags, cacheKey, rv, + getter_AddRefs(channel))) { + return rv; + } + + // Make sure to give the caller a channel if we managed to create one + // This is important for correct error page/session history interaction + if (aRequest) { + NS_ADDREF(*aRequest = channel); + } + + nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadState->Csp(); + if (csp) { + // Check CSP navigate-to + bool allowsNavigateTo = false; + rv = csp->GetAllowsNavigateTo(aLoadState->URI(), + aLoadState->IsFormSubmission(), + false, /* aWasRedirected */ + false, /* aEnforceWhitelist */ + &allowsNavigateTo); + NS_ENSURE_SUCCESS(rv, rv); + + if (!allowsNavigateTo) { + return NS_ERROR_CSP_NAVIGATE_TO_VIOLATION; + } + } + + const nsACString& typeHint = aLoadState->TypeHint(); + if (!typeHint.IsVoid()) { + mContentTypeHint = typeHint; + } else { + mContentTypeHint.Truncate(); + } + + // Load attributes depend on load type... + if (mLoadType == LOAD_RELOAD_CHARSET_CHANGE) { + // Use SetAllowStaleCacheContent (not LOAD_FROM_CACHE flag) since we + // only want to force cache load for this channel, not the whole + // loadGroup. + nsCOMPtr<nsICacheInfoChannel> cachingChannel = do_QueryInterface(channel); + if (cachingChannel) { + cachingChannel->SetAllowStaleCacheContent(true); + } + } + + uint32_t openFlags = + nsDocShell::ComputeURILoaderFlags(mBrowsingContext, mLoadType); + return OpenInitializedChannel(channel, uriLoader, openFlags); +} + +static nsresult AppendSegmentToString(nsIInputStream* aIn, void* aClosure, + const char* aFromRawSegment, + uint32_t aToOffset, uint32_t aCount, + uint32_t* aWriteCount) { + // aFromSegment now contains aCount bytes of data. + + nsAutoCString* buf = static_cast<nsAutoCString*>(aClosure); + buf->Append(aFromRawSegment, aCount); + + // Indicate that we have consumed all of aFromSegment + *aWriteCount = aCount; + return NS_OK; +} + +/* static */ nsresult nsDocShell::AddHeadersToChannel( + nsIInputStream* aHeadersData, nsIChannel* aGenericChannel) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aGenericChannel); + NS_ENSURE_STATE(httpChannel); + + uint32_t numRead; + nsAutoCString headersString; + nsresult rv = aHeadersData->ReadSegments( + AppendSegmentToString, &headersString, UINT32_MAX, &numRead); + NS_ENSURE_SUCCESS(rv, rv); + + // used during the manipulation of the String from the InputStream + nsAutoCString headerName; + nsAutoCString headerValue; + int32_t crlf; + int32_t colon; + + // + // Iterate over the headersString: for each "\r\n" delimited chunk, + // add the value as a header to the nsIHttpChannel + // + + static const char kWhitespace[] = "\b\t\r\n "; + while (true) { + crlf = headersString.Find("\r\n"); + if (crlf == kNotFound) { + return NS_OK; + } + + const nsACString& oneHeader = StringHead(headersString, crlf); + + colon = oneHeader.FindChar(':'); + if (colon == kNotFound) { + return NS_ERROR_UNEXPECTED; + } + + headerName = StringHead(oneHeader, colon); + headerValue = Substring(oneHeader, colon + 1); + + headerName.Trim(kWhitespace); + headerValue.Trim(kWhitespace); + + headersString.Cut(0, crlf + 2); + + // + // FINALLY: we can set the header! + // + + rv = httpChannel->SetRequestHeader(headerName, headerValue, true); + NS_ENSURE_SUCCESS(rv, rv); + } + + MOZ_ASSERT_UNREACHABLE("oops"); + return NS_ERROR_UNEXPECTED; +} + +/* static */ uint32_t nsDocShell::ComputeURILoaderFlags( + BrowsingContext* aBrowsingContext, uint32_t aLoadType) { + MOZ_ASSERT(aBrowsingContext); + + uint32_t openFlags = 0; + if (aLoadType == LOAD_LINK) { + openFlags |= nsIURILoader::IS_CONTENT_PREFERRED; + } + if (!aBrowsingContext->GetAllowContentRetargeting()) { + openFlags |= nsIURILoader::DONT_RETARGET; + } + + return openFlags; +} + +nsresult nsDocShell::OpenInitializedChannel(nsIChannel* aChannel, + nsIURILoader* aURILoader, + uint32_t aOpenFlags) { + nsresult rv = NS_OK; + + // If anything fails here, make sure to clear our initial ClientSource. + auto cleanupInitialClient = + MakeScopeExit([&] { mInitialClientSource.reset(); }); + + nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow(); + NS_ENSURE_TRUE(win, NS_ERROR_FAILURE); + + MaybeCreateInitialClientSource(); + + // Let the client channel helper know if we are using DocumentChannel, + // since redirects get handled in the parent process in that case. + RefPtr<net::DocumentChannel> docChannel = do_QueryObject(aChannel); + if (docChannel && XRE_IsContentProcess()) { + // Tell the content process nsDocumentOpenInfo to not try to do + // any sort of targeting. + aOpenFlags |= nsIURILoader::DONT_RETARGET; + } + + // Since we are loading a document we need to make sure the proper reserved + // and initial client data is stored on the nsILoadInfo. The + // ClientChannelHelper does this and ensures that it is propagated properly + // on redirects. We pass no reserved client here so that the helper will + // create the reserved ClientSource if necessary. + Maybe<ClientInfo> noReservedClient; + if (docChannel) { + // When using DocumentChannel, all redirect handling is done in the parent, + // so we just need the child variant to watch for the internal redirect + // to the final channel. + rv = AddClientChannelHelperInChild( + aChannel, win->EventTargetFor(TaskCategory::Other)); + docChannel->SetInitialClientInfo(GetInitialClientInfo()); + } else { + rv = AddClientChannelHelper(aChannel, std::move(noReservedClient), + GetInitialClientInfo(), + win->EventTargetFor(TaskCategory::Other)); + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = aURILoader->OpenURI(aChannel, aOpenFlags, this); + NS_ENSURE_SUCCESS(rv, rv); + + // We're about to load a new page and it may take time before necko + // gives back any data, so main thread might have a chance to process a + // collector slice + nsJSContext::MaybeRunNextCollectorSlice(this, JS::GCReason::DOCSHELL); + + // Success. Keep the initial ClientSource if it exists. + cleanupInitialClient.release(); + + return NS_OK; +} + +nsresult nsDocShell::OpenRedirectedChannel(nsDocShellLoadState* aLoadState) { + nsCOMPtr<nsIChannel> channel = aLoadState->GetPendingRedirectedChannel(); + MOZ_ASSERT(channel); + + // If anything fails here, make sure to clear our initial ClientSource. + auto cleanupInitialClient = + MakeScopeExit([&] { mInitialClientSource.reset(); }); + + nsCOMPtr<nsPIDOMWindowOuter> win = GetWindow(); + NS_ENSURE_TRUE(win, NS_ERROR_FAILURE); + + MaybeCreateInitialClientSource(); + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + + LoadInfo* li = static_cast<LoadInfo*>(loadInfo.get()); + if (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT) { + li->UpdateBrowsingContextID(mBrowsingContext->Id()); + } else if (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SUBDOCUMENT) { + li->UpdateFrameBrowsingContextID(mBrowsingContext->Id()); + } + // TODO: more attributes need to be updated on the LoadInfo (bug 1561706) + + // If we did a process switch, then we should have an existing allocated + // ClientInfo, so we just need to allocate a corresponding ClientSource. + CreateReservedSourceIfNeeded(channel, + win->EventTargetFor(TaskCategory::Other)); + + RefPtr<nsDocumentOpenInfo> loader = + new nsDocumentOpenInfo(this, nsIURILoader::DONT_RETARGET, nullptr); + channel->SetLoadGroup(mLoadGroup); + + MOZ_ALWAYS_SUCCEEDS(loader->Prepare()); + + nsresult rv = NS_OK; + if (XRE_IsParentProcess()) { + // If we're in the parent, the we don't have an nsIChildChannel, just + // the original channel, which is already open in this process. + + // DocumentLoadListener expects to get an nsIParentChannel, so + // we create a wrapper around the channel and nsIStreamListener + // that forwards functionality as needed, and then we register + // it under the provided identifier. + RefPtr<ParentChannelWrapper> wrapper = + new ParentChannelWrapper(channel, loader); + wrapper->Register(aLoadState->GetPendingRedirectChannelRegistrarId()); + + mLoadGroup->AddRequest(channel, nullptr); + } else if (nsCOMPtr<nsIChildChannel> childChannel = + do_QueryInterface(channel)) { + // Our channel was redirected from another process, so doesn't need to + // be opened again. However, it does need its listener hooked up + // correctly. + rv = childChannel->CompleteRedirectSetup(loader); + } else { + // It's possible for the redirected channel to not implement + // nsIChildChannel and be entirely local (like srcdoc). In that case we + // can just open the local instance and it will work. + rv = channel->AsyncOpen(loader); + } + if (rv == NS_ERROR_NO_CONTENT) { + return NS_OK; + } + NS_ENSURE_SUCCESS(rv, rv); + + // Success. Keep the initial ClientSource if it exists. + cleanupInitialClient.release(); + return NS_OK; +} + +nsresult nsDocShell::ScrollToAnchor(bool aCurHasRef, bool aNewHasRef, + nsACString& aNewHash, uint32_t aLoadType) { + if (!mCurrentURI) { + return NS_OK; + } + + RefPtr<PresShell> presShell = GetPresShell(); + if (!presShell) { + // If we failed to get the shell, or if there is no shell, + // nothing left to do here. + return NS_OK; + } + + nsIScrollableFrame* rootScroll = presShell->GetRootScrollFrameAsScrollable(); + if (rootScroll) { + rootScroll->ClearDidHistoryRestore(); + } + + // If we have no new anchor, we do not want to scroll, unless there is a + // current anchor and we are doing a history load. So return if we have no + // new anchor, and there is no current anchor or the load is not a history + // load. + if ((!aCurHasRef || aLoadType != LOAD_HISTORY) && !aNewHasRef) { + return NS_OK; + } + + // Both the new and current URIs refer to the same page. We can now + // browse to the hash stored in the new URI. + + if (!aNewHash.IsEmpty()) { + // anchor is there, but if it's a load from history, + // we don't have any anchor jumping to do + bool scroll = aLoadType != LOAD_HISTORY && aLoadType != LOAD_RELOAD_NORMAL; + + // We assume that the bytes are in UTF-8, as it says in the + // spec: + // http://www.w3.org/TR/html4/appendix/notes.html#h-B.2.1 + + // We try the UTF-8 string first, and then try the document's + // charset (see below). If the string is not UTF-8, + // conversion will fail and give us an empty Unicode string. + // In that case, we should just fall through to using the + // page's charset. + nsresult rv = NS_ERROR_FAILURE; + NS_ConvertUTF8toUTF16 uStr(aNewHash); + if (!uStr.IsEmpty()) { + rv = presShell->GoToAnchor(uStr, scroll, ScrollFlags::ScrollSmoothAuto); + } + + if (NS_FAILED(rv)) { + char* str = ToNewCString(aNewHash, mozilla::fallible); + if (!str) { + return NS_ERROR_OUT_OF_MEMORY; + } + nsUnescape(str); + NS_ConvertUTF8toUTF16 utf16Str(str); + if (!utf16Str.IsEmpty()) { + rv = presShell->GoToAnchor(utf16Str, scroll, + ScrollFlags::ScrollSmoothAuto); + } + free(str); + } + + // Above will fail if the anchor name is not UTF-8. Need to + // convert from document charset to unicode. + if (NS_FAILED(rv)) { + // Get a document charset + NS_ENSURE_TRUE(mContentViewer, NS_ERROR_FAILURE); + Document* doc = mContentViewer->GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + nsAutoCString charset; + doc->GetDocumentCharacterSet()->Name(charset); + + nsCOMPtr<nsITextToSubURI> textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Unescape and convert to unicode + nsAutoString uStr; + + rv = textToSubURI->UnEscapeAndConvert(charset, aNewHash, uStr); + NS_ENSURE_SUCCESS(rv, rv); + + // Ignore return value of GoToAnchor, since it will return an error + // if there is no such anchor in the document, which is actually a + // success condition for us (we want to update the session history + // with the new URI no matter whether we actually scrolled + // somewhere). + // + // When aNewHash contains "%00", unescaped string may be empty. + // And GoToAnchor asserts if we ask it to scroll to an empty ref. + presShell->GoToAnchor(uStr, scroll && !uStr.IsEmpty(), + ScrollFlags::ScrollSmoothAuto); + } + } else { + // Tell the shell it's at an anchor, without scrolling. + presShell->GoToAnchor(u""_ns, false); + + // An empty anchor was found, but if it's a load from history, + // we don't have to jump to the top of the page. Scrollbar + // position will be restored by the caller, based on positions + // stored in session history. + if (aLoadType == LOAD_HISTORY || aLoadType == LOAD_RELOAD_NORMAL) { + return NS_OK; + } + // An empty anchor. Scroll to the top of the page. Ignore the + // return value; failure to scroll here (e.g. if there is no + // root scrollframe) is not grounds for canceling the load! + SetCurScrollPosEx(0, 0); + } + + return NS_OK; +} + +bool nsDocShell::OnNewURI(nsIURI* aURI, nsIChannel* aChannel, + nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, + bool aFireOnLocationChange, bool aAddToGlobalHistory, + bool aCloneSHChildren) { + MOZ_ASSERT(aURI, "uri is null"); + MOZ_ASSERT(!aChannel || !aTriggeringPrincipal, "Shouldn't have both set"); + + MOZ_ASSERT(!aPrincipalToInherit || + (aPrincipalToInherit && aTriggeringPrincipal)); + +#if defined(DEBUG) + if (MOZ_LOG_TEST(gDocShellLog, LogLevel::Debug)) { + nsAutoCString chanName; + if (aChannel) { + aChannel->GetName(chanName); + } else { + chanName.AssignLiteral("<no channel>"); + } + + MOZ_LOG(gDocShellLog, LogLevel::Debug, + ("nsDocShell[%p]::OnNewURI(\"%s\", [%s], 0x%x)\n", this, + aURI->GetSpecOrDefault().get(), chanName.get(), mLoadType)); + } +#endif + + bool equalUri = false; + + // Get the post data and the HTTP response code from the channel. + uint32_t responseStatus = 0; + nsCOMPtr<nsIInputStream> inputStream; + if (aChannel) { + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel)); + + // Check if the HTTPChannel is hiding under a multiPartChannel + if (!httpChannel) { + GetHttpChannel(aChannel, getter_AddRefs(httpChannel)); + } + + if (httpChannel) { + nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(httpChannel)); + if (uploadChannel) { + uploadChannel->GetUploadStream(getter_AddRefs(inputStream)); + } + + // If the response status indicates an error, unlink this session + // history entry from any entries sharing its document. + nsresult rv = httpChannel->GetResponseStatus(&responseStatus); + if (mLSHE && NS_SUCCEEDED(rv) && responseStatus >= 400) { + mLSHE->AbandonBFCacheEntry(); + // FIXME Do the same for mLoadingEntry + } + } + } + + // Determine if this type of load should update history. + bool updateGHistory = ShouldUpdateGlobalHistory(mLoadType); + + // We don't update session history on reload unless we're loading + // an iframe in shift-reload case. + bool updateSHistory = mBrowsingContext->ShouldUpdateSessionHistory(mLoadType); + + // Create SH Entry (mLSHE) only if there is a SessionHistory object in the + // root browsing context. + // FIXME If session history in the parent is enabled then we only do this if + // the session history object is in process, otherwise we can't really + // use the mLSHE anyway. Once session history is only stored in the + // parent then this code will probably be removed anyway. + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (!rootSH) { + updateSHistory = false; + updateGHistory = false; // XXX Why global history too? + } + + // Check if the url to be loaded is the same as the one already loaded. + if (mCurrentURI) { + aURI->Equals(mCurrentURI, &equalUri); + } + +#ifdef DEBUG + bool shAvailable = (rootSH != nullptr); + + // XXX This log message is almost useless because |updateSHistory| + // and |updateGHistory| are not correct at this point. + + MOZ_LOG(gDocShellLog, LogLevel::Debug, + (" shAvailable=%i updateSHistory=%i updateGHistory=%i" + " equalURI=%i\n", + shAvailable, updateSHistory, updateGHistory, equalUri)); +#endif + + /* If the url to be loaded is the same as the one already there, + * and the original loadType is LOAD_NORMAL, LOAD_LINK, or + * LOAD_STOP_CONTENT, set loadType to LOAD_NORMAL_REPLACE so that + * AddToSessionHistory() won't mess with the current SHEntry and + * if this page has any frame children, it also will be handled + * properly. see bug 83684 + * + * NB: If mOSHE is null but we have a current URI, then it probably + * means that we must be at the transient about:blank content viewer; + * we should let the normal load continue, since there's nothing to + * replace. Sometimes this happens after a session restore (eg process + * switch) and mCurrentURI is not about:blank; we assume we can let the load + * continue (Bug 1301399). + * + * XXX Hopefully changing the loadType at this time will not hurt + * anywhere. The other way to take care of sequentially repeating + * frameset pages is to add new methods to nsIDocShellTreeItem. + * Hopefully I don't have to do that. + */ + if (equalUri && + (mozilla::SessionHistoryInParent() ? !!mActiveEntry : !!mOSHE) && + (mLoadType == LOAD_NORMAL || mLoadType == LOAD_LINK || + mLoadType == LOAD_STOP_CONTENT) && + !inputStream) { + mLoadType = LOAD_NORMAL_REPLACE; + } + + // If this is a refresh to the currently loaded url, we don't + // have to update session or global history. + if (mLoadType == LOAD_REFRESH && !inputStream && equalUri) { + SetHistoryEntryAndUpdateBC(Some<nsISHEntry*>(mOSHE), Nothing()); + } + + /* If the user pressed shift-reload, cache will create a new cache key + * for the page. Save the new cacheKey in Session History. + * see bug 90098 + */ + if (aChannel && IsForceReloadType(mLoadType)) { + MOZ_ASSERT(!updateSHistory || IsSubframe(), + "We shouldn't be updating session history for forced" + " reloads unless we're in a newly created iframe!"); + + nsCOMPtr<nsICacheInfoChannel> cacheChannel(do_QueryInterface(aChannel)); + uint32_t cacheKey = 0; + // Get the Cache Key and store it in SH. + if (cacheChannel) { + cacheChannel->GetCacheKey(&cacheKey); + } + // If we already have a loading history entry, store the new cache key + // in it. Otherwise, since we're doing a reload and won't be updating + // our history entry, store the cache key in our current history entry. + SetCacheKeyOnHistoryEntry(mLSHE ? mLSHE : mOSHE, cacheKey); + + if (!mozilla::SessionHistoryInParent()) { + // Since we're force-reloading, clear all the sub frame history. + ClearFrameHistory(mLSHE); + ClearFrameHistory(mOSHE); + } + } + + if (!mozilla::SessionHistoryInParent()) { + // Clear subframe history on refresh. + // XXX: history.go(0) won't go this path as mLoadType is LOAD_HISTORY in + // this case. One should re-validate after bug 1331865 fixed. + if (mLoadType == LOAD_REFRESH) { + ClearFrameHistory(mLSHE); + ClearFrameHistory(mOSHE); + } + + if (updateSHistory) { + // Update session history if necessary... + if (!mLSHE && (mItemType == typeContent) && mURIResultedInDocument) { + /* This is a fresh page getting loaded for the first time + *.Create a Entry for it and add it to SH, if this is the + * rootDocShell + */ + (void)AddToSessionHistory(aURI, aChannel, aTriggeringPrincipal, + aPrincipalToInherit, + aPartitionedPrincipalToInherit, aCsp, + aCloneSHChildren, getter_AddRefs(mLSHE)); + } + } else if (GetSessionHistory() && mLSHE && mURIResultedInDocument) { + // Even if we don't add anything to SHistory, ensure the current index + // points to the same SHEntry as our mLSHE. + + GetSessionHistory()->LegacySHistory()->EnsureCorrectEntryAtCurrIndex( + mLSHE); + } + } + + // If this is a POST request, we do not want to include this in global + // history. + if (ShouldAddURIVisit(aChannel) && updateGHistory && aAddToGlobalHistory && + !net::ChannelIsPost(aChannel)) { + nsCOMPtr<nsIURI> previousURI; + uint32_t previousFlags = 0; + + if (mLoadType & LOAD_CMD_RELOAD) { + // On a reload request, we don't set redirecting flags. + previousURI = aURI; + } else { + ExtractLastVisit(aChannel, getter_AddRefs(previousURI), &previousFlags); + } + + AddURIVisit(aURI, previousURI, previousFlags, responseStatus); + } + + // If this was a history load or a refresh, or it was a history load but + // later changed to LOAD_NORMAL_REPLACE due to redirection, update the index + // in session history. + if (!mozilla::SessionHistoryInParent() && rootSH && + ((mLoadType & (LOAD_CMD_HISTORY | LOAD_CMD_RELOAD)) || + mLoadType == LOAD_NORMAL_REPLACE || mLoadType == LOAD_REFRESH_REPLACE)) { + mPreviousEntryIndex = rootSH->Index(); + if (!mozilla::SessionHistoryInParent()) { + rootSH->LegacySHistory()->UpdateIndex(); + } + mLoadedEntryIndex = rootSH->Index(); + MOZ_LOG(gPageCacheLog, LogLevel::Verbose, + ("Previous index: %d, Loaded index: %d", mPreviousEntryIndex, + mLoadedEntryIndex)); + } + + // aCloneSHChildren exactly means "we are not loading a new document". + uint32_t locationFlags = + aCloneSHChildren ? uint32_t(LOCATION_CHANGE_SAME_DOCUMENT) : 0; + + bool onLocationChangeNeeded = + SetCurrentURI(aURI, aChannel, aFireOnLocationChange, + /* aIsInitialAboutBlank */ false, locationFlags); + // Make sure to store the referrer from the channel, if any + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel)); + if (httpChannel) { + mReferrerInfo = httpChannel->GetReferrerInfo(); + } + return onLocationChangeNeeded; +} + +Maybe<Wireframe> nsDocShell::GetWireframe() { + const bool collectWireFrame = + mozilla::SessionHistoryInParent() && + StaticPrefs::browser_history_collectWireframes() && + mBrowsingContext->IsTopContent() && mActiveEntry; + + if (!collectWireFrame) { + return Nothing(); + } + + RefPtr<Document> doc = mContentViewer->GetDocument(); + Nullable<Wireframe> wireframe; + doc->GetWireframeWithoutFlushing(false, wireframe); + if (wireframe.IsNull()) { + return Nothing(); + } + return Some(wireframe.Value()); +} + +bool nsDocShell::CollectWireframe() { + Maybe<Wireframe> wireframe = GetWireframe(); + if (wireframe.isNothing()) { + return false; + } + + if (XRE_IsParentProcess()) { + SessionHistoryEntry* entry = + mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry(); + if (entry) { + entry->SetWireframe(wireframe); + } + } else { + mozilla::Unused + << ContentChild::GetSingleton()->SendSessionHistoryEntryWireframe( + mBrowsingContext, wireframe.ref()); + } + + return true; +} + +//***************************************************************************** +// nsDocShell: Session History +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::AddState(JS::Handle<JS::Value> aData, const nsAString& aTitle, + const nsAString& aURL, bool aReplace, JSContext* aCx) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell[%p]: AddState(..., %s, %s, %d)", this, + NS_ConvertUTF16toUTF8(aTitle).get(), + NS_ConvertUTF16toUTF8(aURL).get(), aReplace)); + // Implements History.pushState and History.replaceState + + // Here's what we do, roughly in the order specified by HTML5. The specific + // steps we are executing are at + // <https://html.spec.whatwg.org/multipage/history.html#dom-history-pushstate> + // and + // <https://html.spec.whatwg.org/multipage/history.html#url-and-history-update-steps>. + // This function basically implements #dom-history-pushstate and + // UpdateURLAndHistory implements #url-and-history-update-steps. + // + // A. Serialize aData using structured clone. This is #dom-history-pushstate + // step 5. + // B. If the third argument is present, #dom-history-pushstate step 7. + // 7.1. Resolve the url, relative to our document. + // 7.2. If (a) fails, raise a SECURITY_ERR + // 7.4. Compare the resulting absolute URL to the document's address. If + // any part of the URLs difer other than the <path>, <query>, and + // <fragment> components, raise a SECURITY_ERR and abort. + // C. If !aReplace, #url-and-history-update-steps steps 2.1-2.3: + // Remove from the session history all entries after the current entry, + // as we would after a regular navigation, and save the current + // entry's scroll position (bug 590573). + // D. #url-and-history-update-steps step 2.4 or step 3. As apropriate, + // either add a state object entry to the session history after the + // current entry with the following properties, or modify the current + // session history entry to set + // a. cloned data as the state object, + // b. if the third argument was present, the absolute URL found in + // step 2 + // Also clear the new history entry's POST data (see bug 580069). + // E. If aReplace is false (i.e. we're doing a pushState instead of a + // replaceState), notify bfcache that we've navigated to a new page. + // F. If the third argument is present, set the document's current address + // to the absolute URL found in step B. This is + // #url-and-history-update-steps step 4. + // + // It's important that this function not run arbitrary scripts after step A + // and before completing step E. For example, if a script called + // history.back() before we completed step E, bfcache might destroy an + // active content viewer. Since EvictOutOfRangeContentViewers at the end of + // step E might run script, we can't just put a script blocker around the + // critical section. + // + // Note that we completely ignore the aTitle parameter. + + nsresult rv; + + // Don't clobber the load type of an existing network load. + AutoRestore<uint32_t> loadTypeResetter(mLoadType); + + // pushState effectively becomes replaceState when we've started a network + // load but haven't adopted its document yet. This mirrors what we do with + // changes to the hash at this stage of the game. + if (JustStartedNetworkLoad()) { + aReplace = true; + } + + RefPtr<Document> document = GetDocument(); + NS_ENSURE_TRUE(document, NS_ERROR_FAILURE); + + // Step A: Serialize aData using structured clone. + // https://html.spec.whatwg.org/multipage/history.html#dom-history-pushstate + // step 5. + nsCOMPtr<nsIStructuredCloneContainer> scContainer; + + // scContainer->Init might cause arbitrary JS to run, and this code might + // navigate the page we're on, potentially to a different origin! (bug + // 634834) To protect against this, we abort if our principal changes due + // to the InitFromJSVal() call. + { + RefPtr<Document> origDocument = GetDocument(); + if (!origDocument) { + return NS_ERROR_DOM_SECURITY_ERR; + } + nsCOMPtr<nsIPrincipal> origPrincipal = origDocument->NodePrincipal(); + + scContainer = new nsStructuredCloneContainer(); + rv = scContainer->InitFromJSVal(aData, aCx); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<Document> newDocument = GetDocument(); + if (!newDocument) { + return NS_ERROR_DOM_SECURITY_ERR; + } + nsCOMPtr<nsIPrincipal> newPrincipal = newDocument->NodePrincipal(); + + bool principalsEqual = false; + origPrincipal->Equals(newPrincipal, &principalsEqual); + NS_ENSURE_TRUE(principalsEqual, NS_ERROR_DOM_SECURITY_ERR); + } + + // Check that the state object isn't too long. + int32_t maxStateObjSize = StaticPrefs::browser_history_maxStateObjectSize(); + if (maxStateObjSize < 0) { + maxStateObjSize = 0; + } + + uint64_t scSize; + rv = scContainer->GetSerializedNBytes(&scSize); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(scSize <= (uint32_t)maxStateObjSize, NS_ERROR_ILLEGAL_VALUE); + + // Step B: Resolve aURL. + // https://html.spec.whatwg.org/multipage/history.html#dom-history-pushstate + // step 7. + bool equalURIs = true; + nsCOMPtr<nsIURI> currentURI; + if (mCurrentURI) { + currentURI = nsIOService::CreateExposableURI(mCurrentURI); + } else { + currentURI = mCurrentURI; + } + nsCOMPtr<nsIURI> newURI; + if (aURL.Length() == 0) { + newURI = currentURI; + } else { + // 7.1: Resolve aURL relative to mURI + + nsIURI* docBaseURI = document->GetDocBaseURI(); + if (!docBaseURI) { + return NS_ERROR_FAILURE; + } + + nsAutoCString spec; + docBaseURI->GetSpec(spec); + + rv = NS_NewURI(getter_AddRefs(newURI), aURL, + document->GetDocumentCharacterSet(), docBaseURI); + + // 7.2: If 2a fails, raise a SECURITY_ERR + if (NS_FAILED(rv)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // 7.4 and 7.5: Same-origin check. + if (!nsContentUtils::URIIsLocalFile(newURI)) { + // In addition to checking that the security manager says that + // the new URI has the same origin as our current URI, we also + // check that the two URIs have the same userpass. (The + // security manager says that |http://foo.com| and + // |http://me@foo.com| have the same origin.) currentURI + // won't contain the password part of the userpass, so this + // means that it's never valid to specify a password in a + // pushState or replaceState URI. + + nsCOMPtr<nsIScriptSecurityManager> secMan = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID); + NS_ENSURE_TRUE(secMan, NS_ERROR_FAILURE); + + // It's very important that we check that newURI is of the same + // origin as currentURI, not docBaseURI, because a page can + // set docBaseURI arbitrarily to any domain. + nsAutoCString currentUserPass, newUserPass; + NS_ENSURE_SUCCESS(currentURI->GetUserPass(currentUserPass), + NS_ERROR_FAILURE); + NS_ENSURE_SUCCESS(newURI->GetUserPass(newUserPass), NS_ERROR_FAILURE); + bool isPrivateWin = + document->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > + 0; + if (NS_FAILED(secMan->CheckSameOriginURI(currentURI, newURI, true, + isPrivateWin)) || + !currentUserPass.Equals(newUserPass)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + } else { + // It's a file:// URI + nsCOMPtr<nsIPrincipal> principal = document->GetPrincipal(); + + if (!principal || NS_FAILED(principal->CheckMayLoadWithReporting( + newURI, false, document->InnerWindowID()))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + } + + if (currentURI) { + currentURI->Equals(newURI, &equalURIs); + } else { + equalURIs = false; + } + + } // end of same-origin check + + // Step 8: call "URL and history update steps" + rv = UpdateURLAndHistory(document, newURI, scContainer, aTitle, aReplace, + currentURI, equalURIs); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult nsDocShell::UpdateURLAndHistory(Document* aDocument, nsIURI* aNewURI, + nsIStructuredCloneContainer* aData, + const nsAString& aTitle, bool aReplace, + nsIURI* aCurrentURI, bool aEqualURIs) { + // Implements + // https://html.spec.whatwg.org/multipage/history.html#url-and-history-update-steps + + // If we have a pending title change, handle it before creating a new entry. + aDocument->DoNotifyPossibleTitleChange(); + + // Step 2, if aReplace is false: Create a new entry in the session + // history. This will erase all SHEntries after the new entry and make this + // entry the current one. This operation may modify mOSHE, which we need + // later, so we keep a reference here. + NS_ENSURE_TRUE(mOSHE || mActiveEntry || aReplace, NS_ERROR_FAILURE); + nsCOMPtr<nsISHEntry> oldOSHE = mOSHE; + + // If this push/replaceState changed the document's current URI and the new + // URI differs from the old URI in more than the hash, or if the old + // SHEntry's URI was modified in this way by a push/replaceState call + // set URIWasModified to true for the current SHEntry (bug 669671). + bool sameExceptHashes = true; + aNewURI->EqualsExceptRef(aCurrentURI, &sameExceptHashes); + bool uriWasModified; + if (sameExceptHashes) { + if (mozilla::SessionHistoryInParent()) { + uriWasModified = mActiveEntry && mActiveEntry->GetURIWasModified(); + } else { + uriWasModified = oldOSHE && oldOSHE->GetURIWasModified(); + } + } else { + uriWasModified = true; + } + + mLoadType = LOAD_PUSHSTATE; + + nsCOMPtr<nsISHEntry> newSHEntry; + if (!aReplace) { + // Step 2. + + // Step 2.2, "Remove any tasks queued by the history traversal task + // source that are associated with any Document objects in the + // top-level browsing context's document family." This is very hard in + // SessionHistoryInParent since we can't synchronously access the + // pending navigations that are already sent to the parent. We can + // abort any AsyncGo navigations that are waiting to be sent. If we + // send a message to the parent, it would be processed after any + // navigations previously sent. So long as we consider the "history + // traversal task source" to be the list in this process we match the + // spec. If we move the entire list to the parent, we can handle the + // aborting of loads there, but we don't have a way to synchronously + // remove entries as we do here for non-SHIP. + RefPtr<ChildSHistory> shistory = GetRootSessionHistory(); + if (shistory) { + shistory->RemovePendingHistoryNavigations(); + } + + nsPoint scrollPos = GetCurScrollPos(); + + bool scrollRestorationIsManual; + if (mozilla::SessionHistoryInParent()) { + // FIXME Need to save the current scroll position on mActiveEntry. + scrollRestorationIsManual = mActiveEntry->GetScrollRestorationIsManual(); + } else { + // Save the current scroll position (bug 590573). Step 2.3. + mOSHE->SetScrollPosition(scrollPos.x, scrollPos.y); + + scrollRestorationIsManual = mOSHE->GetScrollRestorationIsManual(); + } + + nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp(); + + if (mozilla::SessionHistoryInParent()) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p UpdateActiveEntry (not replacing)", this)); + nsString title(mActiveEntry->GetTitle()); + UpdateActiveEntry(false, + /* aPreviousScrollPos = */ Some(scrollPos), aNewURI, + /* aOriginalURI = */ nullptr, + /* aReferrerInfo = */ nullptr, + /* aTriggeringPrincipal = */ aDocument->NodePrincipal(), + csp, title, scrollRestorationIsManual, aData, + uriWasModified); + } else { + // Since we're not changing which page we have loaded, pass + // true for aCloneChildren. + nsresult rv = AddToSessionHistory( + aNewURI, nullptr, + aDocument->NodePrincipal(), // triggeringPrincipal + nullptr, nullptr, csp, true, getter_AddRefs(newSHEntry)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(newSHEntry, NS_ERROR_FAILURE); + + // Session history entries created by pushState inherit scroll restoration + // mode from the current entry. + newSHEntry->SetScrollRestorationIsManual(scrollRestorationIsManual); + + nsString title; + mOSHE->GetTitle(title); + + // Set the new SHEntry's title (bug 655273). + newSHEntry->SetTitle(title); + + // Link the new SHEntry to the old SHEntry's BFCache entry, since the + // two entries correspond to the same document. + NS_ENSURE_SUCCESS(newSHEntry->AdoptBFCacheEntry(oldOSHE), + NS_ERROR_FAILURE); + + // AddToSessionHistory may not modify mOSHE. In case it doesn't, + // we'll just set mOSHE here. + mOSHE = newSHEntry; + } + } else if (mozilla::SessionHistoryInParent()) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p UpdateActiveEntry (replacing) mActiveEntry %p", + this, mActiveEntry.get())); + // Setting the resultPrincipalURI to nullptr is fine here: it will cause + // NS_GetFinalChannelURI to use the originalURI as the URI, which is aNewURI + // in our case. We could also set it to aNewURI, with the same result. + // We don't use aTitle here, see bug 544535. + nsString title; + nsCOMPtr<nsIReferrerInfo> referrerInfo; + if (mActiveEntry) { + title = mActiveEntry->GetTitle(); + referrerInfo = mActiveEntry->GetReferrerInfo(); + } else { + referrerInfo = nullptr; + } + UpdateActiveEntry( + true, /* aPreviousScrollPos = */ Nothing(), aNewURI, aNewURI, + /* aReferrerInfo = */ referrerInfo, aDocument->NodePrincipal(), + aDocument->GetCsp(), title, + mActiveEntry && mActiveEntry->GetScrollRestorationIsManual(), aData, + uriWasModified); + } else { + // Step 3. + newSHEntry = mOSHE; + + MOZ_LOG(gSHLog, LogLevel::Debug, ("nsDocShell %p step 3", this)); + // Since we're not changing which page we have loaded, pass + // true for aCloneChildren. + if (!newSHEntry) { + nsresult rv = AddToSessionHistory( + aNewURI, nullptr, + aDocument->NodePrincipal(), // triggeringPrincipal + nullptr, nullptr, aDocument->GetCsp(), true, + getter_AddRefs(newSHEntry)); + NS_ENSURE_SUCCESS(rv, rv); + mOSHE = newSHEntry; + } + + newSHEntry->SetURI(aNewURI); + newSHEntry->SetOriginalURI(aNewURI); + // We replaced the URI of the entry, clear the unstripped URI as it + // shouldn't be used for reloads anymore. + newSHEntry->SetUnstrippedURI(nullptr); + // Setting the resultPrincipalURI to nullptr is fine here: it will cause + // NS_GetFinalChannelURI to use the originalURI as the URI, which is aNewURI + // in our case. We could also set it to aNewURI, with the same result. + newSHEntry->SetResultPrincipalURI(nullptr); + newSHEntry->SetLoadReplace(false); + } + + if (!mozilla::SessionHistoryInParent()) { + // Step 2.4 and 3: Modify new/original session history entry and clear its + // POST data, if there is any. + newSHEntry->SetStateData(aData); + newSHEntry->SetPostData(nullptr); + + newSHEntry->SetURIWasModified(uriWasModified); + + // Step E as described at the top of AddState: If aReplace is false, + // indicating that we're doing a pushState rather than a replaceState, + // notify bfcache that we've added a page to the history so it can evict + // content viewers if appropriate. Otherwise call ReplaceEntry so that we + // notify nsIHistoryListeners that an entry was replaced. We may not have a + // root session history if this call is coming from a document.open() in a + // docshell subtree that disables session history. + RefPtr<ChildSHistory> rootSH = GetRootSessionHistory(); + if (rootSH) { + rootSH->LegacySHistory()->EvictContentViewersOrReplaceEntry(newSHEntry, + aReplace); + } + } + + // Step 4: If the document's URI changed, update document's URI and update + // global history. + // + // We need to call FireOnLocationChange so that the browser's address bar + // gets updated and the back button is enabled, but we only need to + // explicitly call FireOnLocationChange if we're not calling SetCurrentURI, + // since SetCurrentURI will call FireOnLocationChange for us. + // + // Both SetCurrentURI(...) and FireDummyOnLocationChange() pass + // nullptr for aRequest param to FireOnLocationChange(...). Such an update + // notification is allowed only when we know docshell is not loading a new + // document and it requires LOCATION_CHANGE_SAME_DOCUMENT flag. Otherwise, + // FireOnLocationChange(...) breaks security UI. + // + // If the docshell is shutting down, don't update the document URI, as we + // can't load into a docshell that is being destroyed. + if (!aEqualURIs && !mIsBeingDestroyed) { + aDocument->SetDocumentURI(aNewURI); + SetCurrentURI(aNewURI, nullptr, /* aFireLocationChange */ true, + /* aIsInitialAboutBlank */ false, + GetSameDocumentNavigationFlags(aNewURI)); + + AddURIVisit(aNewURI, aCurrentURI, 0); + + // AddURIVisit doesn't set the title for the new URI in global history, + // so do that here. + UpdateGlobalHistoryTitle(aNewURI); + + // Inform the favicon service that our old favicon applies to this new + // URI. + CopyFavicon(aCurrentURI, aNewURI, UsePrivateBrowsing()); + } else { + FireDummyOnLocationChange(); + } + aDocument->SetStateObject(aData); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetCurrentScrollRestorationIsManual(bool* aIsManual) { + if (mozilla::SessionHistoryInParent()) { + *aIsManual = mActiveEntry && mActiveEntry->GetScrollRestorationIsManual(); + return NS_OK; + } + + *aIsManual = false; + if (mOSHE) { + return mOSHE->GetScrollRestorationIsManual(aIsManual); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetCurrentScrollRestorationIsManual(bool aIsManual) { + SetScrollRestorationIsManualOnHistoryEntry(mOSHE, aIsManual); + + return NS_OK; +} + +void nsDocShell::SetScrollRestorationIsManualOnHistoryEntry( + nsISHEntry* aSHEntry, bool aIsManual) { + if (aSHEntry) { + aSHEntry->SetScrollRestorationIsManual(aIsManual); + } + + if (mActiveEntry && mBrowsingContext) { + mActiveEntry->SetScrollRestorationIsManual(aIsManual); + if (XRE_IsParentProcess()) { + SessionHistoryEntry* entry = + mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry(); + if (entry) { + entry->SetScrollRestorationIsManual(aIsManual); + } + } else { + mozilla::Unused << ContentChild::GetSingleton() + ->SendSessionHistoryEntryScrollRestorationIsManual( + mBrowsingContext, aIsManual); + } + } +} + +void nsDocShell::SetCacheKeyOnHistoryEntry(nsISHEntry* aSHEntry, + uint32_t aCacheKey) { + if (aSHEntry) { + aSHEntry->SetCacheKey(aCacheKey); + } + + if (mActiveEntry && mBrowsingContext) { + mActiveEntry->SetCacheKey(aCacheKey); + if (XRE_IsParentProcess()) { + SessionHistoryEntry* entry = + mBrowsingContext->Canonical()->GetActiveSessionHistoryEntry(); + if (entry) { + entry->SetCacheKey(aCacheKey); + } + } else { + mozilla::Unused + << ContentChild::GetSingleton()->SendSessionHistoryEntryCacheKey( + mBrowsingContext, aCacheKey); + } + } +} + +/* static */ +bool nsDocShell::ShouldAddToSessionHistory(nsIURI* aURI, nsIChannel* aChannel) { + // I believe none of the about: urls should go in the history. But then + // that could just be me... If the intent is only deny about:blank then we + // should just do a spec compare, rather than two gets of the scheme and + // then the path. -Gagan + nsresult rv; + nsAutoCString buf; + + rv = aURI->GetScheme(buf); + if (NS_FAILED(rv)) { + return false; + } + + if (buf.EqualsLiteral("about")) { + rv = aURI->GetPathQueryRef(buf); + if (NS_FAILED(rv)) { + return false; + } + + if (buf.EqualsLiteral("blank")) { + return false; + } + // We only want to add about:newtab if it's not privileged, and + // if it is not configured to show the blank page. + if (buf.EqualsLiteral("newtab")) { + if (!StaticPrefs::browser_newtabpage_enabled()) { + return false; + } + + NS_ENSURE_TRUE(aChannel, false); + nsCOMPtr<nsIPrincipal> resultPrincipal; + rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + aChannel, getter_AddRefs(resultPrincipal)); + NS_ENSURE_SUCCESS(rv, false); + return !resultPrincipal->IsSystemPrincipal(); + } + } + + return true; +} + +nsresult nsDocShell::AddToSessionHistory( + nsIURI* aURI, nsIChannel* aChannel, nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, bool aCloneChildren, + nsISHEntry** aNewEntry) { + MOZ_ASSERT(aURI, "uri is null"); + MOZ_ASSERT(!aChannel || !aTriggeringPrincipal, "Shouldn't have both set"); + MOZ_DIAGNOSTIC_ASSERT(!mozilla::SessionHistoryInParent()); + +#if defined(DEBUG) + if (MOZ_LOG_TEST(gDocShellLog, LogLevel::Debug)) { + nsAutoCString chanName; + if (aChannel) { + aChannel->GetName(chanName); + } else { + chanName.AssignLiteral("<no channel>"); + } + + MOZ_LOG(gDocShellLog, LogLevel::Debug, + ("nsDocShell[%p]::AddToSessionHistory(\"%s\", [%s])\n", this, + aURI->GetSpecOrDefault().get(), chanName.get())); + } +#endif + + nsresult rv = NS_OK; + nsCOMPtr<nsISHEntry> entry; + + /* + * If this is a LOAD_FLAGS_REPLACE_HISTORY in a subframe, we use + * the existing SH entry in the page and replace the url and + * other vitalities. + */ + if (LOAD_TYPE_HAS_FLAGS(mLoadType, LOAD_FLAGS_REPLACE_HISTORY) && + !mBrowsingContext->IsTop()) { + // This is a subframe + entry = mOSHE; + if (entry) { + entry->ClearEntry(); + } + } + + // Create a new entry if necessary. + if (!entry) { + entry = new nsSHEntry(); + } + + // Get the post data & referrer + nsCOMPtr<nsIInputStream> inputStream; + nsCOMPtr<nsIURI> originalURI; + nsCOMPtr<nsIURI> resultPrincipalURI; + nsCOMPtr<nsIURI> unstrippedURI; + bool loadReplace = false; + nsCOMPtr<nsIReferrerInfo> referrerInfo; + uint32_t cacheKey = 0; + nsCOMPtr<nsIPrincipal> triggeringPrincipal = aTriggeringPrincipal; + nsCOMPtr<nsIPrincipal> principalToInherit = aPrincipalToInherit; + nsCOMPtr<nsIPrincipal> partitionedPrincipalToInherit = + aPartitionedPrincipalToInherit; + nsCOMPtr<nsIContentSecurityPolicy> csp = aCsp; + bool expired = false; // by default the page is not expired + bool discardLayoutState = false; + nsCOMPtr<nsICacheInfoChannel> cacheChannel; + bool userActivation = false; + + if (aChannel) { + cacheChannel = do_QueryInterface(aChannel); + + /* If there is a caching channel, get the Cache Key and store it + * in SH. + */ + if (cacheChannel) { + cacheChannel->GetCacheKey(&cacheKey); + } + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel)); + + // Check if the httpChannel is hiding under a multipartChannel + if (!httpChannel) { + GetHttpChannel(aChannel, getter_AddRefs(httpChannel)); + } + if (httpChannel) { + nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(httpChannel)); + if (uploadChannel) { + uploadChannel->GetUploadStream(getter_AddRefs(inputStream)); + } + httpChannel->GetOriginalURI(getter_AddRefs(originalURI)); + uint32_t loadFlags; + aChannel->GetLoadFlags(&loadFlags); + loadReplace = loadFlags & nsIChannel::LOAD_REPLACE; + rv = httpChannel->GetReferrerInfo(getter_AddRefs(referrerInfo)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + discardLayoutState = ShouldDiscardLayoutState(httpChannel); + } + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + if (!triggeringPrincipal) { + triggeringPrincipal = loadInfo->TriggeringPrincipal(); + } + if (!csp) { + csp = loadInfo->GetCspToInherit(); + } + + loadInfo->GetResultPrincipalURI(getter_AddRefs(resultPrincipalURI)); + + loadInfo->GetUnstrippedURI(getter_AddRefs(unstrippedURI)); + + userActivation = loadInfo->GetHasValidUserGestureActivation(); + + // For now keep storing just the principal in the SHEntry. + if (!principalToInherit) { + if (loadInfo->GetLoadingSandboxed()) { + if (loadInfo->GetLoadingPrincipal()) { + principalToInherit = NullPrincipal::CreateWithInheritedAttributes( + loadInfo->GetLoadingPrincipal()); + } else { + // get the OriginAttributes + OriginAttributes attrs; + loadInfo->GetOriginAttributes(&attrs); + principalToInherit = NullPrincipal::Create(attrs); + } + } else { + principalToInherit = loadInfo->PrincipalToInherit(); + } + } + + if (!partitionedPrincipalToInherit) { + // XXXehsan is it correct to fall back to the principal to inherit in all + // cases? For example, what about the cases where we are using the load + // info's principal to inherit? Do we need to add a similar concept to + // load info for partitioned principal? + partitionedPrincipalToInherit = principalToInherit; + } + } + + nsAutoString srcdoc; + bool srcdocEntry = false; + nsCOMPtr<nsIURI> baseURI; + + nsCOMPtr<nsIInputStreamChannel> inStrmChan = do_QueryInterface(aChannel); + if (inStrmChan) { + bool isSrcdocChannel; + inStrmChan->GetIsSrcdocChannel(&isSrcdocChannel); + if (isSrcdocChannel) { + inStrmChan->GetSrcdocData(srcdoc); + srcdocEntry = true; + inStrmChan->GetBaseURI(getter_AddRefs(baseURI)); + } else { + srcdoc.SetIsVoid(true); + } + } + /* If cache got a 'no-store', ask SH not to store + * HistoryLayoutState. By default, SH will set this + * flag to true and save HistoryLayoutState. + */ + bool saveLayoutState = !discardLayoutState; + + if (cacheChannel) { + // Check if the page has expired from cache + uint32_t expTime = 0; + cacheChannel->GetCacheTokenExpirationTime(&expTime); + uint32_t now = PRTimeToSeconds(PR_Now()); + if (expTime <= now) { + expired = true; + } + } + + // Title is set in nsDocShell::SetTitle() + entry->Create(aURI, // uri + u""_ns, // Title + inputStream, // Post data stream + cacheKey, // CacheKey + mContentTypeHint, // Content-type + triggeringPrincipal, // Channel or provided principal + principalToInherit, partitionedPrincipalToInherit, csp, + HistoryID(), GetCreatedDynamically(), originalURI, + resultPrincipalURI, unstrippedURI, loadReplace, referrerInfo, + srcdoc, srcdocEntry, baseURI, saveLayoutState, expired, + userActivation); + + if (mBrowsingContext->IsTop() && GetSessionHistory()) { + bool shouldPersist = ShouldAddToSessionHistory(aURI, aChannel); + Maybe<int32_t> previousEntryIndex; + Maybe<int32_t> loadedEntryIndex; + rv = GetSessionHistory()->LegacySHistory()->AddToRootSessionHistory( + aCloneChildren, mOSHE, mBrowsingContext, entry, mLoadType, + shouldPersist, &previousEntryIndex, &loadedEntryIndex); + + MOZ_ASSERT(NS_SUCCEEDED(rv), "Could not add entry to root session history"); + if (previousEntryIndex.isSome()) { + mPreviousEntryIndex = previousEntryIndex.value(); + } + if (loadedEntryIndex.isSome()) { + mLoadedEntryIndex = loadedEntryIndex.value(); + } + + // aCloneChildren implies that we are retaining the same document, thus we + // need to signal to the top WC that the new SHEntry may receive a fresh + // user interaction flag. + if (aCloneChildren) { + WindowContext* topWc = mBrowsingContext->GetTopWindowContext(); + if (topWc && !topWc->IsDiscarded()) { + MOZ_ALWAYS_SUCCEEDS(topWc->SetSHEntryHasUserInteraction(false)); + } + } + } else { + // This is a subframe, make sure that this new SHEntry will be + // marked with user interaction. + WindowContext* topWc = mBrowsingContext->GetTopWindowContext(); + if (topWc && !topWc->IsDiscarded()) { + MOZ_ALWAYS_SUCCEEDS(topWc->SetSHEntryHasUserInteraction(false)); + } + if (!mOSHE || !LOAD_TYPE_HAS_FLAGS(mLoadType, LOAD_FLAGS_REPLACE_HISTORY)) { + rv = AddChildSHEntryToParent(entry, mBrowsingContext->ChildOffset(), + aCloneChildren); + } + } + + // Return the new SH entry... + if (aNewEntry) { + *aNewEntry = nullptr; + if (NS_SUCCEEDED(rv)) { + entry.forget(aNewEntry); + } + } + + return rv; +} + +void nsDocShell::UpdateActiveEntry( + bool aReplace, const Maybe<nsPoint>& aPreviousScrollPos, nsIURI* aURI, + nsIURI* aOriginalURI, nsIReferrerInfo* aReferrerInfo, + nsIPrincipal* aTriggeringPrincipal, nsIContentSecurityPolicy* aCsp, + const nsAString& aTitle, bool aScrollRestorationIsManual, + nsIStructuredCloneContainer* aData, bool aURIWasModified) { + MOZ_ASSERT(mozilla::SessionHistoryInParent()); + MOZ_ASSERT(aURI, "uri is null"); + MOZ_ASSERT(mLoadType == LOAD_PUSHSTATE, + "This code only deals with pushState"); + MOZ_ASSERT_IF(aPreviousScrollPos.isSome(), !aReplace); + + MOZ_LOG(gSHLog, LogLevel::Debug, + ("Creating an active entry on nsDocShell %p to %s", this, + aURI->GetSpecOrDefault().get())); + + // Even if we're replacing an existing entry we create new a + // SessionHistoryInfo. In the parent process we'll keep the existing + // SessionHistoryEntry, but just replace its SessionHistoryInfo, that way the + // entry keeps identity but its data is replaced. + bool replace = aReplace && mActiveEntry; + + if (!replace) { + CollectWireframe(); + } + + if (mActiveEntry) { + // Link this entry to the previous active entry. + mActiveEntry = MakeUnique<SessionHistoryInfo>(*mActiveEntry, aURI); + } else { + mActiveEntry = MakeUnique<SessionHistoryInfo>( + aURI, aTriggeringPrincipal, nullptr, nullptr, aCsp, mContentTypeHint); + } + mActiveEntry->SetOriginalURI(aOriginalURI); + mActiveEntry->SetUnstrippedURI(nullptr); + mActiveEntry->SetReferrerInfo(aReferrerInfo); + mActiveEntry->SetTitle(aTitle); + mActiveEntry->SetStateData(static_cast<nsStructuredCloneContainer*>(aData)); + mActiveEntry->SetURIWasModified(aURIWasModified); + mActiveEntry->SetScrollRestorationIsManual(aScrollRestorationIsManual); + + if (replace) { + mBrowsingContext->ReplaceActiveSessionHistoryEntry(mActiveEntry.get()); + } else { + mBrowsingContext->IncrementHistoryEntryCountForBrowsingContext(); + // FIXME We should probably just compute mChildOffset in the parent + // instead of passing it over IPC here. + mBrowsingContext->SetActiveSessionHistoryEntry( + aPreviousScrollPos, mActiveEntry.get(), mLoadType, + /* aCacheKey = */ 0); + // FIXME Do we need to update mPreviousEntryIndex and mLoadedEntryIndex? + } +} + +nsresult nsDocShell::LoadHistoryEntry(nsISHEntry* aEntry, uint32_t aLoadType, + bool aUserActivation) { + NS_ENSURE_TRUE(aEntry, NS_ERROR_FAILURE); + + nsresult rv; + RefPtr<nsDocShellLoadState> loadState; + rv = aEntry->CreateLoadInfo(getter_AddRefs(loadState)); + NS_ENSURE_SUCCESS(rv, rv); + + // Calling CreateAboutBlankContentViewer can set mOSHE to null, and if + // that's the only thing holding a ref to aEntry that will cause aEntry to + // die while we're loading it. So hold a strong ref to aEntry here, just + // in case. + nsCOMPtr<nsISHEntry> kungFuDeathGrip(aEntry); + + loadState->SetHasValidUserGestureActivation( + loadState->HasValidUserGestureActivation() || aUserActivation); + + return LoadHistoryEntry(loadState, aLoadType, aEntry == mOSHE); +} + +nsresult nsDocShell::LoadHistoryEntry(const LoadingSessionHistoryInfo& aEntry, + uint32_t aLoadType, + bool aUserActivation) { + RefPtr<nsDocShellLoadState> loadState = aEntry.CreateLoadInfo(); + loadState->SetHasValidUserGestureActivation( + loadState->HasValidUserGestureActivation() || aUserActivation); + + return LoadHistoryEntry(loadState, aLoadType, aEntry.mLoadingCurrentEntry); +} + +nsresult nsDocShell::LoadHistoryEntry(nsDocShellLoadState* aLoadState, + uint32_t aLoadType, + bool aLoadingCurrentEntry) { + if (!IsNavigationAllowed()) { + return NS_OK; + } + + // We are setting load type afterwards so we don't have to + // send it in an IPC message + aLoadState->SetLoadType(aLoadType); + + nsresult rv; + if (SchemeIsJavascript(aLoadState->URI())) { + // We're loading a URL that will execute script from inside asyncOpen. + // Replace the current document with about:blank now to prevent + // anything from the current document from leaking into any JavaScript + // code in the URL. + // Don't cache the presentation if we're going to just reload the + // current entry. Caching would lead to trying to save the different + // content viewers in the same nsISHEntry object. + rv = CreateAboutBlankContentViewer( + aLoadState->PrincipalToInherit(), + aLoadState->PartitionedPrincipalToInherit(), nullptr, nullptr, + /* aIsInitialDocument */ false, Nothing(), !aLoadingCurrentEntry); + + if (NS_FAILED(rv)) { + // The creation of the intermittent about:blank content + // viewer failed for some reason (potentially because the + // user prevented it). Interrupt the history load. + return NS_OK; + } + + if (!aLoadState->TriggeringPrincipal()) { + // Ensure that we have a triggeringPrincipal. Otherwise javascript: + // URIs will pick it up from the about:blank page we just loaded, + // and we don't really want even that in this case. + nsCOMPtr<nsIPrincipal> principal = + NullPrincipal::Create(GetOriginAttributes()); + aLoadState->SetTriggeringPrincipal(principal); + } + } + + /* If there is a valid postdata *and* the user pressed + * reload or shift-reload, take user's permission before we + * repost the data to the server. + */ + if ((aLoadType & LOAD_CMD_RELOAD) && aLoadState->PostDataStream()) { + bool repost; + rv = ConfirmRepost(&repost); + if (NS_FAILED(rv)) { + return rv; + } + + // If the user pressed cancel in the dialog, return. We're done here. + if (!repost) { + return NS_BINDING_ABORTED; + } + } + + // If there is no valid triggeringPrincipal, we deny the load + MOZ_ASSERT(aLoadState->TriggeringPrincipal(), + "need a valid triggeringPrincipal to load from history"); + if (!aLoadState->TriggeringPrincipal()) { + return NS_ERROR_FAILURE; + } + + return InternalLoad(aLoadState); // No nsIRequest +} + +NS_IMETHODIMP +nsDocShell::PersistLayoutHistoryState() { + nsresult rv = NS_OK; + + if (mozilla::SessionHistoryInParent() ? !!mActiveEntry : !!mOSHE) { + bool scrollRestorationIsManual; + if (mozilla::SessionHistoryInParent()) { + scrollRestorationIsManual = mActiveEntry->GetScrollRestorationIsManual(); + } else { + scrollRestorationIsManual = mOSHE->GetScrollRestorationIsManual(); + } + nsCOMPtr<nsILayoutHistoryState> layoutState; + if (RefPtr<PresShell> presShell = GetPresShell()) { + rv = presShell->CaptureHistoryState(getter_AddRefs(layoutState)); + } else if (scrollRestorationIsManual) { + // Even if we don't have layout anymore, we may want to reset the + // current scroll state in layout history. + GetLayoutHistoryState(getter_AddRefs(layoutState)); + } + + if (scrollRestorationIsManual && layoutState) { + layoutState->ResetScrollState(); + } + } + + return rv; +} + +void nsDocShell::SwapHistoryEntries(nsISHEntry* aOldEntry, + nsISHEntry* aNewEntry) { + if (aOldEntry == mOSHE) { + mOSHE = aNewEntry; + } + + if (aOldEntry == mLSHE) { + mLSHE = aNewEntry; + } +} + +void nsDocShell::SetHistoryEntryAndUpdateBC(const Maybe<nsISHEntry*>& aLSHE, + const Maybe<nsISHEntry*>& aOSHE) { + // We want to hold on to the reference in mLSHE before we update it. + // Otherwise, SetHistoryEntry could release the last reference to + // the entry while aOSHE is pointing to it. + nsCOMPtr<nsISHEntry> deathGripOldLSHE; + if (aLSHE.isSome()) { + deathGripOldLSHE = SetHistoryEntry(&mLSHE, aLSHE.value()); + MOZ_ASSERT(mLSHE.get() == aLSHE.value()); + } + nsCOMPtr<nsISHEntry> deathGripOldOSHE; + if (aOSHE.isSome()) { + deathGripOldOSHE = SetHistoryEntry(&mOSHE, aOSHE.value()); + MOZ_ASSERT(mOSHE.get() == aOSHE.value()); + } +} + +already_AddRefed<nsISHEntry> nsDocShell::SetHistoryEntry( + nsCOMPtr<nsISHEntry>* aPtr, nsISHEntry* aEntry) { + // We need to sync up the docshell and session history trees for + // subframe navigation. If the load was in a subframe, we forward up to + // the root docshell, which will then recursively sync up all docshells + // to their corresponding entries in the new session history tree. + // If we don't do this, then we can cache a content viewer on the wrong + // cloned entry, and subsequently restore it at the wrong time. + RefPtr<BrowsingContext> topBC = mBrowsingContext->Top(); + if (topBC->IsDiscarded()) { + topBC = nullptr; + } + RefPtr<BrowsingContext> currBC = + mBrowsingContext->IsDiscarded() ? nullptr : mBrowsingContext; + if (topBC && *aPtr) { + (*aPtr)->SyncTreesForSubframeNavigation(aEntry, topBC, currBC); + } + nsCOMPtr<nsISHEntry> entry(aEntry); + entry.swap(*aPtr); + return entry.forget(); +} + +already_AddRefed<ChildSHistory> nsDocShell::GetRootSessionHistory() { + RefPtr<ChildSHistory> childSHistory = + mBrowsingContext->Top()->GetChildSessionHistory(); + return childSHistory.forget(); +} + +nsresult nsDocShell::GetHttpChannel(nsIChannel* aChannel, + nsIHttpChannel** aReturn) { + NS_ENSURE_ARG_POINTER(aReturn); + if (!aChannel) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIMultiPartChannel> multiPartChannel(do_QueryInterface(aChannel)); + if (multiPartChannel) { + nsCOMPtr<nsIChannel> baseChannel; + multiPartChannel->GetBaseChannel(getter_AddRefs(baseChannel)); + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(baseChannel)); + *aReturn = httpChannel; + NS_IF_ADDREF(*aReturn); + } + return NS_OK; +} + +bool nsDocShell::ShouldDiscardLayoutState(nsIHttpChannel* aChannel) { + // By default layout State will be saved. + if (!aChannel) { + return false; + } + + // figure out if SH should be saving layout state + bool noStore = false; + Unused << aChannel->IsNoStoreResponse(&noStore); + return noStore; +} + +NS_IMETHODIMP +nsDocShell::GetEditor(nsIEditor** aEditor) { + NS_ENSURE_ARG_POINTER(aEditor); + RefPtr<HTMLEditor> htmlEditor = GetHTMLEditorInternal(); + htmlEditor.forget(aEditor); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetEditor(nsIEditor* aEditor) { + HTMLEditor* htmlEditor = aEditor ? aEditor->GetAsHTMLEditor() : nullptr; + // If TextEditor comes, throw an error. + if (aEditor && !htmlEditor) { + return NS_ERROR_INVALID_ARG; + } + return SetHTMLEditorInternal(htmlEditor); +} + +HTMLEditor* nsDocShell::GetHTMLEditorInternal() { + return mEditorData ? mEditorData->GetHTMLEditor() : nullptr; +} + +nsresult nsDocShell::SetHTMLEditorInternal(HTMLEditor* aHTMLEditor) { + if (!aHTMLEditor && !mEditorData) { + return NS_OK; + } + + nsresult rv = EnsureEditorData(); + if (NS_FAILED(rv)) { + return rv; + } + + return mEditorData->SetHTMLEditor(aHTMLEditor); +} + +NS_IMETHODIMP +nsDocShell::GetEditable(bool* aEditable) { + NS_ENSURE_ARG_POINTER(aEditable); + *aEditable = mEditorData && mEditorData->GetEditable(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetHasEditingSession(bool* aHasEditingSession) { + NS_ENSURE_ARG_POINTER(aHasEditingSession); + + if (mEditorData) { + *aHasEditingSession = !!mEditorData->GetEditingSession(); + } else { + *aHasEditingSession = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::MakeEditable(bool aInWaitForUriLoad) { + nsresult rv = EnsureEditorData(); + if (NS_FAILED(rv)) { + return rv; + } + + return mEditorData->MakeEditable(aInWaitForUriLoad); +} + +/* static */ bool nsDocShell::ShouldAddURIVisit(nsIChannel* aChannel) { + bool needToAddURIVisit = true; + nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aChannel)); + if (props) { + mozilla::Unused << props->GetPropertyAsBool( + u"docshell.needToAddURIVisit"_ns, &needToAddURIVisit); + } + + return needToAddURIVisit; +} + +/* static */ void nsDocShell::ExtractLastVisit( + nsIChannel* aChannel, nsIURI** aURI, uint32_t* aChannelRedirectFlags) { + nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aChannel)); + if (!props) { + return; + } + + nsresult rv; + nsCOMPtr<nsIURI> uri(do_GetProperty(props, u"docshell.previousURI"_ns, &rv)); + if (NS_SUCCEEDED(rv)) { + uri.forget(aURI); + + rv = props->GetPropertyAsUint32(u"docshell.previousFlags"_ns, + aChannelRedirectFlags); + + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "Could not fetch previous flags, URI will be treated like referrer"); + + } else { + // There is no last visit for this channel, so this must be the first + // link. Link the visit to the referrer of this request, if any. + // Treat referrer as null if there is an error getting it. + NS_GetReferrerFromChannel(aChannel, aURI); + } +} + +void nsDocShell::SaveLastVisit(nsIChannel* aChannel, nsIURI* aURI, + uint32_t aChannelRedirectFlags) { + nsCOMPtr<nsIWritablePropertyBag2> props(do_QueryInterface(aChannel)); + if (!props || !aURI) { + return; + } + + props->SetPropertyAsInterface(u"docshell.previousURI"_ns, aURI); + props->SetPropertyAsUint32(u"docshell.previousFlags"_ns, + aChannelRedirectFlags); +} + +/* static */ void nsDocShell::InternalAddURIVisit( + nsIURI* aURI, nsIURI* aPreviousURI, uint32_t aChannelRedirectFlags, + uint32_t aResponseStatus, BrowsingContext* aBrowsingContext, + nsIWidget* aWidget, uint32_t aLoadType) { + MOZ_ASSERT(aURI, "Visited URI is null!"); + MOZ_ASSERT(aLoadType != LOAD_ERROR_PAGE && aLoadType != LOAD_BYPASS_HISTORY, + "Do not add error or bypass pages to global history"); + + bool usePrivateBrowsing = false; + aBrowsingContext->GetUsePrivateBrowsing(&usePrivateBrowsing); + + // Only content-type docshells save URI visits. Also don't do + // anything here if we're not supposed to use global history. + if (!aBrowsingContext->IsContent() || + !aBrowsingContext->GetUseGlobalHistory() || usePrivateBrowsing) { + return; + } + + nsCOMPtr<IHistory> history = components::History::Service(); + + if (history) { + uint32_t visitURIFlags = 0; + + if (aBrowsingContext->IsTop()) { + visitURIFlags |= IHistory::TOP_LEVEL; + } + + if (aChannelRedirectFlags & nsIChannelEventSink::REDIRECT_TEMPORARY) { + visitURIFlags |= IHistory::REDIRECT_TEMPORARY; + } else if (aChannelRedirectFlags & + nsIChannelEventSink::REDIRECT_PERMANENT) { + visitURIFlags |= IHistory::REDIRECT_PERMANENT; + } else { + MOZ_ASSERT(!aChannelRedirectFlags, + "One of REDIRECT_TEMPORARY or REDIRECT_PERMANENT must be set " + "if any flags in aChannelRedirectFlags is set."); + } + + if (aResponseStatus >= 300 && aResponseStatus < 400) { + visitURIFlags |= IHistory::REDIRECT_SOURCE; + if (aResponseStatus == 301 || aResponseStatus == 308) { + visitURIFlags |= IHistory::REDIRECT_SOURCE_PERMANENT; + } + } + // Errors 400-501 and 505 are considered unrecoverable, in the sense a + // simple retry attempt by the user is unlikely to solve them. + // 408 is special cased, since may actually indicate a temporary + // connection problem. + else if (aResponseStatus != 408 && + ((aResponseStatus >= 400 && aResponseStatus <= 501) || + aResponseStatus == 505)) { + visitURIFlags |= IHistory::UNRECOVERABLE_ERROR; + } + + mozilla::Unused << history->VisitURI(aWidget, aURI, aPreviousURI, + visitURIFlags, + aBrowsingContext->BrowserId()); + } +} + +void nsDocShell::AddURIVisit(nsIURI* aURI, nsIURI* aPreviousURI, + uint32_t aChannelRedirectFlags, + uint32_t aResponseStatus) { + nsPIDOMWindowOuter* outer = GetWindow(); + nsCOMPtr<nsIWidget> widget = widget::WidgetUtils::DOMWindowToWidget(outer); + + InternalAddURIVisit(aURI, aPreviousURI, aChannelRedirectFlags, + aResponseStatus, mBrowsingContext, widget, mLoadType); +} + +//***************************************************************************** +// nsDocShell: Helper Routines +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::SetLoadType(uint32_t aLoadType) { + mLoadType = aLoadType; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetLoadType(uint32_t* aLoadType) { + *aLoadType = mLoadType; + return NS_OK; +} + +nsresult nsDocShell::ConfirmRepost(bool* aRepost) { + if (StaticPrefs::dom_confirm_repost_testing_always_accept()) { + *aRepost = true; + return NS_OK; + } + + nsCOMPtr<nsIPromptCollection> prompter = + do_GetService("@mozilla.org/embedcomp/prompt-collection;1"); + if (!prompter) { + return NS_ERROR_NOT_AVAILABLE; + } + + return prompter->ConfirmRepost(mBrowsingContext, aRepost); +} + +nsresult nsDocShell::GetPromptAndStringBundle(nsIPrompt** aPrompt, + nsIStringBundle** aStringBundle) { + NS_ENSURE_SUCCESS(GetInterface(NS_GET_IID(nsIPrompt), (void**)aPrompt), + NS_ERROR_FAILURE); + + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(stringBundleService, NS_ERROR_FAILURE); + + NS_ENSURE_SUCCESS( + stringBundleService->CreateBundle(kAppstringsBundleURL, aStringBundle), + NS_ERROR_FAILURE); + + return NS_OK; +} + +nsIScrollableFrame* nsDocShell::GetRootScrollFrame() { + PresShell* presShell = GetPresShell(); + NS_ENSURE_TRUE(presShell, nullptr); + + return presShell->GetRootScrollFrameAsScrollable(); +} + +nsresult nsDocShell::EnsureScriptEnvironment() { + if (mScriptGlobal) { + return NS_OK; + } + + if (mIsBeingDestroyed) { + return NS_ERROR_NOT_AVAILABLE; + } + +#ifdef DEBUG + NS_ASSERTION(!mInEnsureScriptEnv, + "Infinite loop! Calling EnsureScriptEnvironment() from " + "within EnsureScriptEnvironment()!"); + + // Yeah, this isn't re-entrant safe, but that's ok since if we + // re-enter this method, we'll infinitely loop... + AutoRestore<bool> boolSetter(mInEnsureScriptEnv); + mInEnsureScriptEnv = true; +#endif + + nsCOMPtr<nsIWebBrowserChrome> browserChrome(do_GetInterface(mTreeOwner)); + NS_ENSURE_TRUE(browserChrome, NS_ERROR_NOT_AVAILABLE); + + uint32_t chromeFlags; + browserChrome->GetChromeFlags(&chromeFlags); + + // If our window is modal and we're not opened as chrome, make + // this window a modal content window. + mScriptGlobal = nsGlobalWindowOuter::Create(this, mItemType == typeChrome); + MOZ_ASSERT(mScriptGlobal); + + // Ensure the script object is set up to run script. + return mScriptGlobal->EnsureScriptEnvironment(); +} + +nsresult nsDocShell::EnsureEditorData() { + MOZ_ASSERT(!mIsBeingDestroyed); + + bool openDocHasDetachedEditor = mOSHE && mOSHE->HasDetachedEditor(); + if (!mEditorData && !mIsBeingDestroyed && !openDocHasDetachedEditor) { + // We shouldn't recreate the editor data if it already exists, or + // we're shutting down, or we already have a detached editor data + // stored in the session history. We should only have one editordata + // per docshell. + mEditorData = MakeUnique<nsDocShellEditorData>(this); + } + + return mEditorData ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsDocShell::EnsureFind() { + if (!mFind) { + mFind = new nsWebBrowserFind(); + } + + // we promise that the nsIWebBrowserFind that we return has been set + // up to point to the focused, or content window, so we have to + // set that up each time. + + nsIScriptGlobalObject* scriptGO = GetScriptGlobalObject(); + NS_ENSURE_TRUE(scriptGO, NS_ERROR_UNEXPECTED); + + // default to our window + nsCOMPtr<nsPIDOMWindowOuter> ourWindow = do_QueryInterface(scriptGO); + nsCOMPtr<nsPIDOMWindowOuter> windowToSearch; + nsFocusManager::GetFocusedDescendant(ourWindow, + nsFocusManager::eIncludeAllDescendants, + getter_AddRefs(windowToSearch)); + + nsCOMPtr<nsIWebBrowserFindInFrames> findInFrames = do_QueryInterface(mFind); + if (!findInFrames) { + return NS_ERROR_NO_INTERFACE; + } + + nsresult rv = findInFrames->SetRootSearchFrame(ourWindow); + if (NS_FAILED(rv)) { + return rv; + } + rv = findInFrames->SetCurrentSearchFrame(windowToSearch); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::IsBeingDestroyed(bool* aDoomed) { + NS_ENSURE_ARG(aDoomed); + *aDoomed = mIsBeingDestroyed; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetIsExecutingOnLoadHandler(bool* aResult) { + NS_ENSURE_ARG(aResult); + *aResult = mIsExecutingOnLoadHandler; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetLayoutHistoryState(nsILayoutHistoryState** aLayoutHistoryState) { + nsCOMPtr<nsILayoutHistoryState> state; + if (mozilla::SessionHistoryInParent()) { + if (mActiveEntry) { + state = mActiveEntry->GetLayoutHistoryState(); + } + } else { + if (mOSHE) { + state = mOSHE->GetLayoutHistoryState(); + } + } + state.forget(aLayoutHistoryState); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetLayoutHistoryState(nsILayoutHistoryState* aLayoutHistoryState) { + if (mOSHE) { + mOSHE->SetLayoutHistoryState(aLayoutHistoryState); + } + if (mActiveEntry) { + mActiveEntry->SetLayoutHistoryState(aLayoutHistoryState); + } + return NS_OK; +} + +nsDocShell::InterfaceRequestorProxy::InterfaceRequestorProxy( + nsIInterfaceRequestor* aRequestor) { + if (aRequestor) { + mWeakPtr = do_GetWeakReference(aRequestor); + } +} + +nsDocShell::InterfaceRequestorProxy::~InterfaceRequestorProxy() { + mWeakPtr = nullptr; +} + +NS_IMPL_ISUPPORTS(nsDocShell::InterfaceRequestorProxy, nsIInterfaceRequestor) + +NS_IMETHODIMP +nsDocShell::InterfaceRequestorProxy::GetInterface(const nsIID& aIID, + void** aSink) { + NS_ENSURE_ARG_POINTER(aSink); + nsCOMPtr<nsIInterfaceRequestor> ifReq = do_QueryReferent(mWeakPtr); + if (ifReq) { + return ifReq->GetInterface(aIID, aSink); + } + *aSink = nullptr; + return NS_NOINTERFACE; +} + +//***************************************************************************** +// nsDocShell::nsIAuthPromptProvider +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::GetAuthPrompt(uint32_t aPromptReason, const nsIID& aIID, + void** aResult) { + // a priority prompt request will override a false mAllowAuth setting + bool priorityPrompt = (aPromptReason == PROMPT_PROXY); + + if (!mAllowAuth && !priorityPrompt) { + return NS_ERROR_NOT_AVAILABLE; + } + + // we're either allowing auth, or it's a proxy request + nsresult rv; + nsCOMPtr<nsIPromptFactory> wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnsureScriptEnvironment(); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the an auth prompter for our window so that the parenting + // of the dialogs works as it should when using tabs. + + return wwatch->GetPrompt(mScriptGlobal, aIID, + reinterpret_cast<void**>(aResult)); +} + +//***************************************************************************** +// nsDocShell::nsILoadContext +//***************************************************************************** + +NS_IMETHODIMP +nsDocShell::GetAssociatedWindow(mozIDOMWindowProxy** aWindow) { + CallGetInterface(this, aWindow); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetTopWindow(mozIDOMWindowProxy** aWindow) { + return mBrowsingContext->GetTopWindow(aWindow); +} + +NS_IMETHODIMP +nsDocShell::GetTopFrameElement(Element** aElement) { + return mBrowsingContext->GetTopFrameElement(aElement); +} + +NS_IMETHODIMP +nsDocShell::GetUseTrackingProtection(bool* aUseTrackingProtection) { + return mBrowsingContext->GetUseTrackingProtection(aUseTrackingProtection); +} + +NS_IMETHODIMP +nsDocShell::SetUseTrackingProtection(bool aUseTrackingProtection) { + return mBrowsingContext->SetUseTrackingProtection(aUseTrackingProtection); +} + +NS_IMETHODIMP +nsDocShell::GetIsContent(bool* aIsContent) { + *aIsContent = (mItemType == typeContent); + return NS_OK; +} + +bool nsDocShell::IsOKToLoadURI(nsIURI* aURI) { + MOZ_ASSERT(aURI, "Must have a URI!"); + + if (!mFiredUnloadEvent) { + return true; + } + + if (!mLoadingURI) { + return false; + } + + bool isPrivateWin = false; + Document* doc = GetDocument(); + if (doc) { + isPrivateWin = + doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > 0; + } + + nsCOMPtr<nsIScriptSecurityManager> secMan = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID); + return secMan && NS_SUCCEEDED(secMan->CheckSameOriginURI( + aURI, mLoadingURI, false, isPrivateWin)); +} + +// +// Routines for selection and clipboard +// +nsresult nsDocShell::GetControllerForCommand(const char* aCommand, + nsIController** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = nullptr; + + NS_ENSURE_TRUE(mScriptGlobal, NS_ERROR_FAILURE); + + nsCOMPtr<nsPIWindowRoot> root = mScriptGlobal->GetTopWindowRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); + + return root->GetControllerForCommand(aCommand, false /* for any window */, + aResult); +} + +NS_IMETHODIMP +nsDocShell::IsCommandEnabled(const char* aCommand, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; + + nsresult rv = NS_ERROR_FAILURE; + + nsCOMPtr<nsIController> controller; + rv = GetControllerForCommand(aCommand, getter_AddRefs(controller)); + if (controller) { + rv = controller->IsCommandEnabled(aCommand, aResult); + } + + return rv; +} + +NS_IMETHODIMP +nsDocShell::DoCommand(const char* aCommand) { + nsresult rv = NS_ERROR_FAILURE; + + nsCOMPtr<nsIController> controller; + rv = GetControllerForCommand(aCommand, getter_AddRefs(controller)); + if (controller) { + rv = controller->DoCommand(aCommand); + } + + return rv; +} + +NS_IMETHODIMP +nsDocShell::DoCommandWithParams(const char* aCommand, + nsICommandParams* aParams) { + nsCOMPtr<nsIController> controller; + nsresult rv = GetControllerForCommand(aCommand, getter_AddRefs(controller)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsICommandController> commandController = + do_QueryInterface(controller, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return commandController->DoCommandWithParams(aCommand, aParams); +} + +nsresult nsDocShell::EnsureCommandHandler() { + if (!mCommandManager) { + if (nsCOMPtr<nsPIDOMWindowOuter> domWindow = GetWindow()) { + mCommandManager = new nsCommandManager(domWindow); + } + } + return mCommandManager ? NS_OK : NS_ERROR_FAILURE; +} + +// link handling + +class OnLinkClickEvent : public Runnable { + public: + OnLinkClickEvent(nsDocShell* aHandler, nsIContent* aContent, + nsDocShellLoadState* aLoadState, bool aNoOpenerImplied, + bool aIsTrusted, nsIPrincipal* aTriggeringPrincipal); + + NS_IMETHOD Run() override { + AutoPopupStatePusher popupStatePusher(mPopupState); + + // We need to set up an AutoJSAPI here for the following reason: When we + // do OnLinkClickSync we'll eventually end up in + // nsGlobalWindow::OpenInternal which only does popup blocking if + // !LegacyIsCallerChromeOrNativeCode(). So we need to fake things so that + // we don't look like native code as far as LegacyIsCallerNativeCode() is + // concerned. + AutoJSAPI jsapi; + if (mIsTrusted || jsapi.Init(mContent->OwnerDoc()->GetScopeObject())) { + mHandler->OnLinkClickSync(mContent, mLoadState, mNoOpenerImplied, + mTriggeringPrincipal); + } + return NS_OK; + } + + private: + RefPtr<nsDocShell> mHandler; + nsCOMPtr<nsIContent> mContent; + RefPtr<nsDocShellLoadState> mLoadState; + nsCOMPtr<nsIPrincipal> mTriggeringPrincipal; + PopupBlocker::PopupControlState mPopupState; + bool mNoOpenerImplied; + bool mIsTrusted; +}; + +OnLinkClickEvent::OnLinkClickEvent(nsDocShell* aHandler, nsIContent* aContent, + nsDocShellLoadState* aLoadState, + bool aNoOpenerImplied, bool aIsTrusted, + nsIPrincipal* aTriggeringPrincipal) + : mozilla::Runnable("OnLinkClickEvent"), + mHandler(aHandler), + mContent(aContent), + mLoadState(aLoadState), + mTriggeringPrincipal(aTriggeringPrincipal), + mPopupState(PopupBlocker::GetPopupControlState()), + mNoOpenerImplied(aNoOpenerImplied), + mIsTrusted(aIsTrusted) {} + +nsresult nsDocShell::OnLinkClick( + nsIContent* aContent, nsIURI* aURI, const nsAString& aTargetSpec, + const nsAString& aFileName, nsIInputStream* aPostDataStream, + nsIInputStream* aHeadersDataStream, bool aIsUserTriggered, bool aIsTrusted, + nsIPrincipal* aTriggeringPrincipal, nsIContentSecurityPolicy* aCsp) { +#ifndef ANDROID + MOZ_ASSERT(aTriggeringPrincipal, "Need a valid triggeringPrincipal"); +#endif + NS_ASSERTION(NS_IsMainThread(), "wrong thread"); + + if (!IsNavigationAllowed() || !IsOKToLoadURI(aURI)) { + return NS_OK; + } + + // On history navigation through Back/Forward buttons, don't execute + // automatic JavaScript redirection such as |anchorElement.click()| or + // |formElement.submit()|. + // + // XXX |formElement.submit()| bypasses this checkpoint because it calls + // nsDocShell::OnLinkClickSync(...) instead. + if (ShouldBlockLoadingForBackButton()) { + return NS_OK; + } + + if (aContent->IsEditable()) { + return NS_OK; + } + + Document* ownerDoc = aContent->OwnerDoc(); + if (nsContentUtils::IsExternalProtocol(aURI)) { + ownerDoc->EnsureNotEnteringAndExitFullscreen(); + } + + bool noOpenerImplied = false; + nsAutoString target(aTargetSpec); + if (aFileName.IsVoid() && + ShouldOpenInBlankTarget(aTargetSpec, aURI, aContent, aIsUserTriggered)) { + target = u"_blank"; + if (!aTargetSpec.Equals(target)) { + noOpenerImplied = true; + } + } + + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aURI); + loadState->SetTarget(target); + loadState->SetFileName(aFileName); + loadState->SetPostDataStream(aPostDataStream); + loadState->SetHeadersStream(aHeadersDataStream); + loadState->SetFirstParty(true); + loadState->SetTriggeringPrincipal( + aTriggeringPrincipal ? aTriggeringPrincipal : aContent->NodePrincipal()); + loadState->SetPrincipalToInherit(aContent->NodePrincipal()); + loadState->SetCsp(aCsp ? aCsp : aContent->GetCsp()); + loadState->SetAllowFocusMove(UserActivation::IsHandlingUserInput()); + + nsCOMPtr<nsIRunnable> ev = + new OnLinkClickEvent(this, aContent, loadState, noOpenerImplied, + aIsTrusted, aTriggeringPrincipal); + return Dispatch(TaskCategory::UI, ev.forget()); +} + +bool nsDocShell::ShouldOpenInBlankTarget(const nsAString& aOriginalTarget, + nsIURI* aLinkURI, nsIContent* aContent, + bool aIsUserTriggered) { + if (net::SchemeIsJavascript(aLinkURI)) { + return false; + } + + // External links from within app tabs should always open in new tabs + // instead of replacing the app tab's page (Bug 575561) + // nsIURI.host can throw for non-nsStandardURL nsIURIs. If we fail to + // get either host, just return false to use the original target. + nsAutoCString linkHost; + if (NS_FAILED(aLinkURI->GetHost(linkHost))) { + return false; + } + + // The targetTopLevelLinkClicksToBlank property on BrowsingContext allows + // privileged code to change the default targeting behaviour. In particular, + // if a user-initiated link click for the (or targetting the) top-level frame + // is detected, we default the target to "_blank" to give it a new + // top-level BrowsingContext. + if (mBrowsingContext->TargetTopLevelLinkClicksToBlank() && aIsUserTriggered && + ((aOriginalTarget.IsEmpty() && mBrowsingContext->IsTop()) || + aOriginalTarget == u"_top"_ns)) { + return true; + } + + // Don't modify non-default targets. + if (!aOriginalTarget.IsEmpty()) { + return false; + } + + // Only check targets that are in extension panels or app tabs. + // (isAppTab will be false for app tab subframes). + nsString mmGroup = mBrowsingContext->Top()->GetMessageManagerGroup(); + if (!mmGroup.EqualsLiteral("webext-browsers") && + !mBrowsingContext->IsAppTab()) { + return false; + } + + nsCOMPtr<nsIURI> docURI = aContent->OwnerDoc()->GetDocumentURIObject(); + if (!docURI) { + return false; + } + + nsAutoCString docHost; + if (NS_FAILED(docURI->GetHost(docHost))) { + return false; + } + + if (linkHost.Equals(docHost)) { + return false; + } + + // Special case: ignore "www" prefix if it is part of host string + return linkHost.Length() < docHost.Length() + ? !docHost.Equals("www."_ns + linkHost) + : !linkHost.Equals("www."_ns + docHost); +} + +static bool ElementCanHaveNoopener(nsIContent* aContent) { + // Make sure we are dealing with either an <A>, <AREA>, or <FORM> element in + // the HTML, XHTML, or SVG namespace. + return aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area, + nsGkAtoms::form) || + aContent->IsSVGElement(nsGkAtoms::a); +} + +nsresult nsDocShell::OnLinkClickSync(nsIContent* aContent, + nsDocShellLoadState* aLoadState, + bool aNoOpenerImplied, + nsIPrincipal* aTriggeringPrincipal) { + if (!IsNavigationAllowed() || !IsOKToLoadURI(aLoadState->URI())) { + return NS_OK; + } + + // XXX When the linking node was HTMLFormElement, it is synchronous event. + // That is, the caller of this method is not |OnLinkClickEvent::Run()| + // but |HTMLFormElement::SubmitSubmission(...)|. + if (aContent->IsHTMLElement(nsGkAtoms::form) && + ShouldBlockLoadingForBackButton()) { + return NS_OK; + } + + if (aContent->IsEditable()) { + return NS_OK; + } + + // if the triggeringPrincipal is not passed explicitly, then we + // fall back to using doc->NodePrincipal() as the triggeringPrincipal. + nsCOMPtr<nsIPrincipal> triggeringPrincipal = + aTriggeringPrincipal ? aTriggeringPrincipal : aContent->NodePrincipal(); + + { + // defer to an external protocol handler if necessary... + nsCOMPtr<nsIExternalProtocolService> extProtService = + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID); + if (extProtService) { + nsAutoCString scheme; + aLoadState->URI()->GetScheme(scheme); + if (!scheme.IsEmpty()) { + // if the URL scheme does not correspond to an exposed protocol, then + // we need to hand this link click over to the external protocol + // handler. + bool isExposed; + nsresult rv = + extProtService->IsExposedProtocol(scheme.get(), &isExposed); + if (NS_SUCCEEDED(rv) && !isExposed) { + return extProtService->LoadURI( + aLoadState->URI(), triggeringPrincipal, nullptr, mBrowsingContext, + /* aTriggeredExternally */ + false, + /* aHasValidUserGestureActivation */ + aContent->OwnerDoc()->HasValidTransientUserGestureActivation()); + } + } + } + } + uint32_t triggeringSandboxFlags = 0; + if (mBrowsingContext) { + triggeringSandboxFlags = aContent->OwnerDoc()->GetSandboxFlags(); + } + + uint32_t flags = INTERNAL_LOAD_FLAGS_NONE; + bool elementCanHaveNoopener = ElementCanHaveNoopener(aContent); + bool triggeringPrincipalIsSystemPrincipal = + aLoadState->TriggeringPrincipal()->IsSystemPrincipal(); + if (elementCanHaveNoopener) { + MOZ_ASSERT(aContent->IsHTMLElement() || aContent->IsSVGElement()); + nsAutoString relString; + aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::rel, + relString); + nsWhitespaceTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tok( + relString); + + bool targetBlank = aLoadState->Target().LowerCaseEqualsLiteral("_blank"); + bool explicitOpenerSet = false; + + // The opener behaviour follows a hierarchy, such that if a higher + // priority behaviour is specified, it always takes priority. That + // priority is currently: norefrerer > noopener > opener > default + + while (tok.hasMoreTokens()) { + const nsAString& token = tok.nextToken(); + if (token.LowerCaseEqualsLiteral("noreferrer")) { + flags |= INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER | + INTERNAL_LOAD_FLAGS_NO_OPENER; + // noreferrer cannot be overwritten by a 'rel=opener'. + explicitOpenerSet = true; + break; + } + + if (token.LowerCaseEqualsLiteral("noopener")) { + flags |= INTERNAL_LOAD_FLAGS_NO_OPENER; + explicitOpenerSet = true; + } + + if (targetBlank && StaticPrefs::dom_targetBlankNoOpener_enabled() && + token.LowerCaseEqualsLiteral("opener") && !explicitOpenerSet) { + explicitOpenerSet = true; + } + } + + if (targetBlank && StaticPrefs::dom_targetBlankNoOpener_enabled() && + !explicitOpenerSet && !triggeringPrincipalIsSystemPrincipal) { + flags |= INTERNAL_LOAD_FLAGS_NO_OPENER; + } + + if (aNoOpenerImplied) { + flags |= INTERNAL_LOAD_FLAGS_NO_OPENER; + } + } + + // Get the owner document of the link that was clicked, this will be + // the document that the link is in, or the last document that the + // link was in. From that document, we'll get the URI to use as the + // referrer, since the current URI in this docshell may be a + // new document that we're in the process of loading. + RefPtr<Document> referrerDoc = aContent->OwnerDoc(); + + // Now check that the referrerDoc's inner window is the current inner + // window for mScriptGlobal. If it's not, then we don't want to + // follow this link. + nsPIDOMWindowInner* referrerInner = referrerDoc->GetInnerWindow(); + if (!mScriptGlobal || !referrerInner || + mScriptGlobal->GetCurrentInnerWindow() != referrerInner) { + // We're no longer the current inner window + return NS_OK; + } + + // referrer could be null here in some odd cases, but that's ok, + // we'll just load the link w/o sending a referrer in those cases. + + // If this is an anchor element, grab its type property to use as a hint + nsAutoString typeHint; + RefPtr<HTMLAnchorElement> anchor = HTMLAnchorElement::FromNode(aContent); + if (anchor) { + anchor->GetType(typeHint); + NS_ConvertUTF16toUTF8 utf8Hint(typeHint); + nsAutoCString type, dummy; + NS_ParseRequestContentType(utf8Hint, type, dummy); + CopyUTF8toUTF16(type, typeHint); + } + + // Link click (or form submission) can be triggered inside an onload + // handler, and we don't want to add history entry in this case. + bool inOnLoadHandler = false; + GetIsExecutingOnLoadHandler(&inOnLoadHandler); + uint32_t loadType = inOnLoadHandler ? LOAD_NORMAL_REPLACE : LOAD_LINK; + + nsCOMPtr<nsIReferrerInfo> referrerInfo = + elementCanHaveNoopener ? new ReferrerInfo(*aContent->AsElement()) + : new ReferrerInfo(*referrerDoc); + RefPtr<WindowContext> context = mBrowsingContext->GetCurrentWindowContext(); + + aLoadState->SetTriggeringSandboxFlags(triggeringSandboxFlags); + aLoadState->SetReferrerInfo(referrerInfo); + aLoadState->SetInternalLoadFlags(flags); + aLoadState->SetTypeHint(NS_ConvertUTF16toUTF8(typeHint)); + aLoadState->SetLoadType(loadType); + aLoadState->SetSourceBrowsingContext(mBrowsingContext); + aLoadState->SetHasValidUserGestureActivation( + context && context->HasValidTransientUserGestureActivation()); + + nsresult rv = InternalLoad(aLoadState); + + if (NS_SUCCEEDED(rv)) { + nsPingListener::DispatchPings(this, aContent, aLoadState->URI(), + referrerInfo); + } + + return rv; +} + +nsresult nsDocShell::OnOverLink(nsIContent* aContent, nsIURI* aURI, + const nsAString& aTargetSpec) { + if (aContent->IsEditable()) { + return NS_OK; + } + + nsresult rv = NS_ERROR_FAILURE; + + nsCOMPtr<nsIWebBrowserChrome> browserChrome = do_GetInterface(mTreeOwner); + if (!browserChrome) { + return rv; + } + + nsCOMPtr<nsIURI> exposableURI = nsIOService::CreateExposableURI(aURI); + nsAutoCString spec; + rv = exposableURI->GetDisplaySpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ConvertUTF8toUTF16 uStr(spec); + + PredictorPredict(aURI, mCurrentURI, nsINetworkPredictor::PREDICT_LINK, + aContent->NodePrincipal()->OriginAttributesRef(), nullptr); + + rv = browserChrome->SetLinkStatus(uStr); + return rv; +} + +nsresult nsDocShell::OnLeaveLink() { + nsCOMPtr<nsIWebBrowserChrome> browserChrome(do_GetInterface(mTreeOwner)); + nsresult rv = NS_ERROR_FAILURE; + + if (browserChrome) { + rv = browserChrome->SetLinkStatus(u""_ns); + } + return rv; +} + +bool nsDocShell::ShouldBlockLoadingForBackButton() { + if (!(mLoadType & LOAD_CMD_HISTORY) || + UserActivation::IsHandlingUserInput() || + !Preferences::GetBool("accessibility.blockjsredirection")) { + return false; + } + + bool canGoForward = false; + GetCanGoForward(&canGoForward); + return canGoForward; +} + +bool nsDocShell::PluginsAllowedInCurrentDoc() { + if (!mContentViewer) { + return false; + } + + Document* doc = mContentViewer->GetDocument(); + if (!doc) { + return false; + } + + return doc->GetAllowPlugins(); +} + +//---------------------------------------------------------------------- +// Web Shell Services API + +// This functions is only called when a new charset is detected in loading a +// document. +nsresult nsDocShell::CharsetChangeReloadDocument( + mozilla::NotNull<const mozilla::Encoding*> aEncoding, int32_t aSource) { + // XXX hack. keep the aCharset and aSource wait to pick it up + nsCOMPtr<nsIContentViewer> cv; + NS_ENSURE_SUCCESS(GetContentViewer(getter_AddRefs(cv)), NS_ERROR_FAILURE); + if (cv) { + int32_t source; + Unused << cv->GetReloadEncodingAndSource(&source); + if (aSource > source) { + cv->SetReloadEncodingAndSource(aEncoding, aSource); + if (eCharsetReloadRequested != mCharsetReloadState) { + mCharsetReloadState = eCharsetReloadRequested; + switch (mLoadType) { + case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE: + return Reload(LOAD_FLAGS_CHARSET_CHANGE | LOAD_FLAGS_BYPASS_CACHE | + LOAD_FLAGS_BYPASS_PROXY); + case LOAD_RELOAD_BYPASS_CACHE: + return Reload(LOAD_FLAGS_CHARSET_CHANGE | LOAD_FLAGS_BYPASS_CACHE); + default: + return Reload(LOAD_FLAGS_CHARSET_CHANGE); + } + } + } + } + // return failure if this request is not accepted due to mCharsetReloadState + return NS_ERROR_DOCSHELL_REQUEST_REJECTED; +} + +nsresult nsDocShell::CharsetChangeStopDocumentLoad() { + if (eCharsetReloadRequested != mCharsetReloadState) { + Stop(nsIWebNavigation::STOP_ALL); + return NS_OK; + } + // return failer if this request is not accepted due to mCharsetReloadState + return NS_ERROR_DOCSHELL_REQUEST_REJECTED; +} + +NS_IMETHODIMP nsDocShell::ExitPrintPreview() { +#if NS_PRINT_PREVIEW + nsCOMPtr<nsIWebBrowserPrint> viewer = do_QueryInterface(mContentViewer); + return viewer->ExitPrintPreview(); +#else + return NS_OK; +#endif +} + +/* [infallible] */ +NS_IMETHODIMP nsDocShell::GetIsTopLevelContentDocShell( + bool* aIsTopLevelContentDocShell) { + *aIsTopLevelContentDocShell = false; + + if (mItemType == typeContent) { + *aIsTopLevelContentDocShell = mBrowsingContext->IsTopContent(); + } + + return NS_OK; +} + +// Implements nsILoadContext.originAttributes +NS_IMETHODIMP +nsDocShell::GetScriptableOriginAttributes(JSContext* aCx, + JS::MutableHandle<JS::Value> aVal) { + return mBrowsingContext->GetScriptableOriginAttributes(aCx, aVal); +} + +// Implements nsIDocShell.GetOriginAttributes() +NS_IMETHODIMP +nsDocShell::GetOriginAttributes(JSContext* aCx, + JS::MutableHandle<JS::Value> aVal) { + return mBrowsingContext->GetScriptableOriginAttributes(aCx, aVal); +} + +bool nsDocShell::ServiceWorkerAllowedToControlWindow(nsIPrincipal* aPrincipal, + nsIURI* aURI) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aURI); + + if (UsePrivateBrowsing() || mBrowsingContext->GetSandboxFlags()) { + return false; + } + + nsCOMPtr<nsIDocShellTreeItem> parent; + GetInProcessSameTypeParent(getter_AddRefs(parent)); + nsPIDOMWindowOuter* parentOuter = parent ? parent->GetWindow() : nullptr; + nsPIDOMWindowInner* parentInner = + parentOuter ? parentOuter->GetCurrentInnerWindow() : nullptr; + + StorageAccess storage = + StorageAllowedForNewWindow(aPrincipal, aURI, parentInner); + + // If the partitioned service worker is enabled, service worker is allowed to + // control the window if partition is enabled. + if (StaticPrefs::privacy_partition_serviceWorkers() && parentInner) { + RefPtr<Document> doc = parentInner->GetExtantDoc(); + + if (doc && StoragePartitioningEnabled(storage, doc->CookieJarSettings())) { + return true; + } + } + + return storage == StorageAccess::eAllow; +} + +nsresult nsDocShell::SetOriginAttributes(const OriginAttributes& aAttrs) { + MOZ_ASSERT(!mIsBeingDestroyed); + return mBrowsingContext->SetOriginAttributes(aAttrs); +} + +NS_IMETHODIMP +nsDocShell::ResumeRedirectedLoad(uint64_t aIdentifier, int32_t aHistoryIndex) { + RefPtr<nsDocShell> self = this; + RefPtr<ChildProcessChannelListener> cpcl = + ChildProcessChannelListener::GetSingleton(); + + // Call into InternalLoad with the pending channel when it is received. + cpcl->RegisterCallback( + aIdentifier, [self, aHistoryIndex]( + nsDocShellLoadState* aLoadState, + nsTArray<Endpoint<extensions::PStreamFilterParent>>&& + aStreamFilterEndpoints, + nsDOMNavigationTiming* aTiming) { + MOZ_ASSERT(aLoadState->GetPendingRedirectedChannel()); + if (NS_WARN_IF(self->mIsBeingDestroyed)) { + aLoadState->GetPendingRedirectedChannel()->CancelWithReason( + NS_BINDING_ABORTED, "nsDocShell::mIsBeingDestroyed"_ns); + return NS_BINDING_ABORTED; + } + + self->mLoadType = aLoadState->LoadType(); + nsCOMPtr<nsIURI> previousURI; + uint32_t previousFlags = 0; + ExtractLastVisit(aLoadState->GetPendingRedirectedChannel(), + getter_AddRefs(previousURI), &previousFlags); + self->SaveLastVisit(aLoadState->GetPendingRedirectedChannel(), + previousURI, previousFlags); + + if (aTiming) { + self->mTiming = new nsDOMNavigationTiming(self, aTiming); + self->mBlankTiming = false; + } + + // If we're performing a history load, locate the correct history entry, + // and set the relevant bits on our loadState. + if (aHistoryIndex >= 0 && self->GetSessionHistory() && + !mozilla::SessionHistoryInParent()) { + nsCOMPtr<nsISHistory> legacySHistory = + self->GetSessionHistory()->LegacySHistory(); + + nsCOMPtr<nsISHEntry> entry; + nsresult rv = legacySHistory->GetEntryAtIndex(aHistoryIndex, + getter_AddRefs(entry)); + if (NS_SUCCEEDED(rv)) { + legacySHistory->InternalSetRequestedIndex(aHistoryIndex); + aLoadState->SetLoadType(LOAD_HISTORY); + aLoadState->SetSHEntry(entry); + } + } + + self->InternalLoad(aLoadState); + + if (aLoadState->GetOriginalURIString().isSome()) { + // Save URI string in case it's needed later when + // sending to search engine service in EndPageLoad() + self->mOriginalUriString = *aLoadState->GetOriginalURIString(); + } + + for (auto& endpoint : aStreamFilterEndpoints) { + extensions::StreamFilterParent::Attach( + aLoadState->GetPendingRedirectedChannel(), std::move(endpoint)); + } + + // If the channel isn't pending, then it means that InternalLoad + // never connected it, and we shouldn't try to continue. This + // can happen even if InternalLoad returned NS_OK. + bool pending = false; + aLoadState->GetPendingRedirectedChannel()->IsPending(&pending); + NS_ASSERTION(pending, "We should have connected the pending channel!"); + if (!pending) { + return NS_BINDING_ABORTED; + } + return NS_OK; + }); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::SetOriginAttributes(JS::Handle<JS::Value> aOriginAttributes, + JSContext* aCx) { + OriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + return SetOriginAttributes(attrs); +} + +NS_IMETHODIMP +nsDocShell::GetAsyncPanZoomEnabled(bool* aOut) { + if (PresShell* presShell = GetPresShell()) { + *aOut = presShell->AsyncPanZoomEnabled(); + return NS_OK; + } + + // If we don't have a presShell, fall back to the default platform value of + // whether or not APZ is enabled. + *aOut = gfxPlatform::AsyncPanZoomEnabled(); + return NS_OK; +} + +bool nsDocShell::HasUnloadedParent() { + for (WindowContext* wc = GetBrowsingContext()->GetParentWindowContext(); wc; + wc = wc->GetParentWindowContext()) { + if (!wc->IsCurrent() || wc->IsDiscarded() || + wc->GetBrowsingContext()->IsDiscarded()) { + // If a parent is OOP and the parent WindowContext is no + // longer current, we can assume the parent was unloaded. + return true; + } + + if (wc->GetBrowsingContext()->IsInProcess() && + (!wc->GetBrowsingContext()->GetDocShell() || + wc->GetBrowsingContext()->GetDocShell()->GetIsInUnload())) { + return true; + } + } + return false; +} + +/* static */ +bool nsDocShell::ShouldUpdateGlobalHistory(uint32_t aLoadType) { + return !(aLoadType == LOAD_BYPASS_HISTORY || aLoadType == LOAD_ERROR_PAGE || + aLoadType & LOAD_CMD_HISTORY); +} + +void nsDocShell::UpdateGlobalHistoryTitle(nsIURI* aURI) { + if (!mBrowsingContext->GetUseGlobalHistory() || UsePrivateBrowsing()) { + return; + } + + // Global history is interested into sub-frame visits only for link-coloring + // purposes, thus title updates are skipped for those. + // + // Moreover, some iframe documents (such as the ones created via + // document.open()) inherit the document uri of the caller, which would cause + // us to override a previously set page title with one from the subframe. + if (IsSubframe()) { + return; + } + + if (nsCOMPtr<IHistory> history = components::History::Service()) { + history->SetURITitle(aURI, mTitle); + } +} + +bool nsDocShell::IsInvisible() { return mInvisible; } + +void nsDocShell::SetInvisible(bool aInvisible) { mInvisible = aInvisible; } + +// The caller owns |aAsyncCause| here. +void nsDocShell::NotifyJSRunToCompletionStart(const char* aReason, + const nsAString& aFunctionName, + const nsAString& aFilename, + const uint32_t aLineNumber, + JS::Handle<JS::Value> aAsyncStack, + const char* aAsyncCause) { + // If first start, mark interval start. + if (mJSRunToCompletionDepth == 0 && TimelineConsumers::HasConsumer(this)) { + TimelineConsumers::AddMarkerForDocShell( + this, mozilla::MakeUnique<JavascriptTimelineMarker>( + aReason, aFunctionName, aFilename, aLineNumber, + MarkerTracingType::START, aAsyncStack, aAsyncCause)); + } + + mJSRunToCompletionDepth++; +} + +void nsDocShell::NotifyJSRunToCompletionStop() { + mJSRunToCompletionDepth--; + + // If last stop, mark interval end. + if (mJSRunToCompletionDepth == 0 && TimelineConsumers::HasConsumer(this)) { + TimelineConsumers::AddMarkerForDocShell(this, "Javascript", + MarkerTracingType::END); + } +} + +/* static */ +void nsDocShell::MaybeNotifyKeywordSearchLoading(const nsString& aProvider, + const nsString& aKeyword) { + if (aProvider.IsEmpty()) { + return; + } + nsresult rv; + nsCOMPtr<nsISupportsString> isupportsString = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = isupportsString->SetData(aProvider); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService(); + if (obsSvc) { + // Note that "keyword-search" refers to a search via the url + // bar, not a bookmarks keyword search. + obsSvc->NotifyObservers(isupportsString, "keyword-search", aKeyword.get()); + } +} + +NS_IMETHODIMP +nsDocShell::ShouldPrepareForIntercept(nsIURI* aURI, nsIChannel* aChannel, + bool* aShouldIntercept) { + return mInterceptController->ShouldPrepareForIntercept(aURI, aChannel, + aShouldIntercept); +} + +NS_IMETHODIMP +nsDocShell::ChannelIntercepted(nsIInterceptedChannel* aChannel) { + return mInterceptController->ChannelIntercepted(aChannel); +} + +bool nsDocShell::InFrameSwap() { + RefPtr<nsDocShell> shell = this; + do { + if (shell->mInFrameSwap) { + return true; + } + shell = shell->GetInProcessParentDocshell(); + } while (shell); + return false; +} + +UniquePtr<ClientSource> nsDocShell::TakeInitialClientSource() { + return std::move(mInitialClientSource); +} + +NS_IMETHODIMP +nsDocShell::GetEditingSession(nsIEditingSession** aEditSession) { + if (!NS_SUCCEEDED(EnsureEditorData())) { + return NS_ERROR_FAILURE; + } + + *aEditSession = do_AddRef(mEditorData->GetEditingSession()).take(); + return *aEditSession ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsDocShell::GetScriptableBrowserChild(nsIBrowserChild** aBrowserChild) { + *aBrowserChild = GetBrowserChild().take(); + return *aBrowserChild ? NS_OK : NS_ERROR_FAILURE; +} + +already_AddRefed<nsIBrowserChild> nsDocShell::GetBrowserChild() { + nsCOMPtr<nsIBrowserChild> tc = do_QueryReferent(mBrowserChild); + return tc.forget(); +} + +nsCommandManager* nsDocShell::GetCommandManager() { + NS_ENSURE_SUCCESS(EnsureCommandHandler(), nullptr); + return mCommandManager; +} + +NS_IMETHODIMP_(void) +nsDocShell::GetOriginAttributes(mozilla::OriginAttributes& aAttrs) { + mBrowsingContext->GetOriginAttributes(aAttrs); +} + +HTMLEditor* nsIDocShell::GetHTMLEditor() { + nsDocShell* docShell = static_cast<nsDocShell*>(this); + return docShell->GetHTMLEditorInternal(); +} + +nsresult nsIDocShell::SetHTMLEditor(HTMLEditor* aHTMLEditor) { + nsDocShell* docShell = static_cast<nsDocShell*>(this); + return docShell->SetHTMLEditorInternal(aHTMLEditor); +} + +#define MATRIX_LENGTH 20 + +NS_IMETHODIMP +nsDocShell::SetColorMatrix(const nsTArray<float>& aMatrix) { + if (aMatrix.Length() == MATRIX_LENGTH) { + mColorMatrix.reset(new gfx::Matrix5x4()); + static_assert( + MATRIX_LENGTH * sizeof(float) == sizeof(mColorMatrix->components), + "Size mismatch for our memcpy"); + memcpy(mColorMatrix->components, aMatrix.Elements(), + sizeof(mColorMatrix->components)); + } else if (aMatrix.Length() == 0) { + mColorMatrix.reset(); + } else { + return NS_ERROR_INVALID_ARG; + } + + PresShell* presShell = GetPresShell(); + if (!presShell) { + return NS_ERROR_FAILURE; + } + + nsIFrame* frame = presShell->GetRootFrame(); + if (!frame) { + return NS_ERROR_FAILURE; + } + + frame->SchedulePaint(); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetColorMatrix(nsTArray<float>& aMatrix) { + if (mColorMatrix) { + aMatrix.SetLength(MATRIX_LENGTH); + static_assert( + MATRIX_LENGTH * sizeof(float) == sizeof(mColorMatrix->components), + "Size mismatch for our memcpy"); + memcpy(aMatrix.Elements(), mColorMatrix->components, + MATRIX_LENGTH * sizeof(float)); + } + + return NS_OK; +} + +#undef MATRIX_LENGTH + +NS_IMETHODIMP +nsDocShell::GetIsForceReloading(bool* aForceReload) { + *aForceReload = IsForceReloading(); + return NS_OK; +} + +bool nsDocShell::IsForceReloading() { return IsForceReloadType(mLoadType); } + +NS_IMETHODIMP +nsDocShell::GetBrowsingContextXPCOM(BrowsingContext** aBrowsingContext) { + *aBrowsingContext = do_AddRef(mBrowsingContext).take(); + return NS_OK; +} + +BrowsingContext* nsDocShell::GetBrowsingContext() { return mBrowsingContext; } + +bool nsDocShell::GetIsAttemptingToNavigate() { + // XXXbz the document.open spec says to abort even if there's just a + // queued navigation task, sort of. It's not clear whether browsers + // actually do that, and we didn't use to do it, so for now let's + // not do that. + // https://github.com/whatwg/html/issues/3447 tracks the spec side of this. + if (mDocumentRequest) { + // There's definitely a navigation in progress. + return true; + } + + // javascript: channels have slightly weird behavior: they're LOAD_BACKGROUND + // until the script runs, which means they're not sending loadgroup + // notifications and hence not getting set as mDocumentRequest. Look through + // our loadgroup for document-level javascript: loads. + if (!mLoadGroup) { + return false; + } + + nsCOMPtr<nsISimpleEnumerator> requests; + mLoadGroup->GetRequests(getter_AddRefs(requests)); + bool hasMore = false; + while (NS_SUCCEEDED(requests->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsISupports> elem; + requests->GetNext(getter_AddRefs(elem)); + nsCOMPtr<nsIScriptChannel> scriptChannel(do_QueryInterface(elem)); + if (!scriptChannel) { + continue; + } + + if (scriptChannel->GetIsDocumentLoad()) { + // This is a javascript: load that might lead to a new document, + // hence a navigation. + return true; + } + } + + return mCheckingSessionHistory; +} + +void nsDocShell::SetLoadingSessionHistoryInfo( + const mozilla::dom::LoadingSessionHistoryInfo& aLoadingInfo, + bool aNeedToReportActiveAfterLoadingBecomesActive) { + // FIXME Would like to assert this, but can't yet. + // MOZ_ASSERT(!mLoadingEntry); + MOZ_LOG(gSHLog, LogLevel::Debug, + ("Setting the loading entry on nsDocShell %p to %s", this, + aLoadingInfo.mInfo.GetURI()->GetSpecOrDefault().get())); + mLoadingEntry = MakeUnique<LoadingSessionHistoryInfo>(aLoadingInfo); + mNeedToReportActiveAfterLoadingBecomesActive = + aNeedToReportActiveAfterLoadingBecomesActive; +} + +void nsDocShell::MoveLoadingToActiveEntry(bool aPersist, bool aExpired, + uint32_t aCacheKey, + nsIURI* aPreviousURI) { + MOZ_ASSERT(mozilla::SessionHistoryInParent()); + + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell %p MoveLoadingToActiveEntry", this)); + + UniquePtr<SessionHistoryInfo> previousActiveEntry(mActiveEntry.release()); + mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> loadingEntry; + mActiveEntryIsLoadingFromSessionHistory = + mLoadingEntry && mLoadingEntry->mLoadIsFromSessionHistory; + if (mLoadingEntry) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("Moving the loading entry to the active entry on nsDocShell %p " + "to %s", + this, mLoadingEntry->mInfo.GetURI()->GetSpecOrDefault().get())); + mActiveEntry = MakeUnique<SessionHistoryInfo>(mLoadingEntry->mInfo); + mLoadingEntry.swap(loadingEntry); + if (!mActiveEntryIsLoadingFromSessionHistory) { + if (mNeedToReportActiveAfterLoadingBecomesActive) { + // Needed to pass various history length WPTs. + mBrowsingContext->SetActiveSessionHistoryEntry( + mozilla::Nothing(), mActiveEntry.get(), mLoadType, + /* aUpdatedCacheKey = */ 0, false); + } + mBrowsingContext->IncrementHistoryEntryCountForBrowsingContext(); + } + } + mNeedToReportActiveAfterLoadingBecomesActive = false; + + if (mActiveEntry) { + if (aCacheKey != 0) { + mActiveEntry->SetCacheKey(aCacheKey); + } + MOZ_ASSERT(loadingEntry); + uint32_t loadType = + mLoadType == LOAD_ERROR_PAGE ? mFailedLoadType : mLoadType; + + if (loadingEntry->mLoadId != UINT64_MAX) { + // We're passing in mCurrentURI, which could be null. SessionHistoryCommit + // does require a non-null uri if this is for a refresh load of the same + // URI, but in that case mCurrentURI won't be null here. + mBrowsingContext->SessionHistoryCommit( + *loadingEntry, loadType, aPreviousURI, previousActiveEntry.get(), + aPersist, false, aExpired, aCacheKey); + } + } +} + +static bool IsFaviconLoad(nsIRequest* aRequest) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (!channel) { + return false; + } + + nsCOMPtr<nsILoadInfo> li = channel->LoadInfo(); + return li && li->InternalContentPolicyType() == + nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON; +} + +void nsDocShell::RecordSingleChannelId(bool aStartRequest, + nsIRequest* aRequest) { + // Ignore favicon loads, they don't need to block caching. + if (IsFaviconLoad(aRequest)) { + return; + } + + MOZ_ASSERT_IF(!aStartRequest, mRequestForBlockingFromBFCacheCount > 0); + + mRequestForBlockingFromBFCacheCount += aStartRequest ? 1 : -1; + + if (mBrowsingContext->GetCurrentWindowContext()) { + // We have three states: no request, one request with an id and + // eiher one request without an id or multiple requests. Nothing() is no + // request, Some(non-zero) is one request with an id and Some(0) is one + // request without an id or multiple requests. + Maybe<uint64_t> singleChannelId; + if (mRequestForBlockingFromBFCacheCount > 1) { + singleChannelId = Some(0); + } else if (mRequestForBlockingFromBFCacheCount == 1) { + nsCOMPtr<nsIIdentChannel> identChannel; + if (aStartRequest) { + identChannel = do_QueryInterface(aRequest); + } else { + // aChannel is the channel that's being removed, but we need to check if + // the remaining channel in the loadgroup has an id. + nsCOMPtr<nsISimpleEnumerator> requests; + mLoadGroup->GetRequests(getter_AddRefs(requests)); + for (const auto& request : SimpleEnumerator<nsIRequest>(requests)) { + if (!IsFaviconLoad(request) && + !!(identChannel = do_QueryInterface(request))) { + break; + } + } + } + + if (identChannel) { + singleChannelId = Some(identChannel->ChannelId()); + } else { + singleChannelId = Some(0); + } + } else { + MOZ_ASSERT(mRequestForBlockingFromBFCacheCount == 0); + singleChannelId = Nothing(); + } + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Verbose))) { + nsAutoCString uri("[no uri]"); + if (mCurrentURI) { + uri = mCurrentURI->GetSpecOrDefault(); + } + if (singleChannelId.isNothing()) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Verbose, + ("Loadgroup for %s doesn't have any requests relevant for " + "blocking BFCache", + uri.get())); + } else if (singleChannelId.value() == 0) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Verbose, + ("Loadgroup for %s has multiple requests relevant for blocking " + "BFCache", + uri.get())); + } else { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Verbose, + ("Loadgroup for %s has one request with id %" PRIu64 + " relevant for blocking BFCache", + uri.get(), singleChannelId.value())); + } + } + + if (mSingleChannelId != singleChannelId) { + mSingleChannelId = singleChannelId; + WindowGlobalChild* wgc = + mBrowsingContext->GetCurrentWindowContext()->GetWindowGlobalChild(); + if (wgc) { + wgc->SendSetSingleChannelId(singleChannelId); + } + } + } +} + +NS_IMETHODIMP +nsDocShell::OnStartRequest(nsIRequest* aRequest) { + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Verbose))) { + nsAutoCString uri("[no uri]"); + if (mCurrentURI) { + uri = mCurrentURI->GetSpecOrDefault(); + } + nsAutoCString name; + aRequest->GetName(name); + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Verbose, + ("Adding request %s to loadgroup for %s", name.get(), uri.get())); + } + RecordSingleChannelId(true, aRequest); + return nsDocLoader::OnStartRequest(aRequest); +} + +NS_IMETHODIMP +nsDocShell::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gSHIPBFCacheLog, LogLevel::Verbose))) { + nsAutoCString uri("[no uri]"); + if (mCurrentURI) { + uri = mCurrentURI->GetSpecOrDefault(); + } + nsAutoCString name; + aRequest->GetName(name); + MOZ_LOG( + gSHIPBFCacheLog, LogLevel::Verbose, + ("Removing request %s from loadgroup for %s", name.get(), uri.get())); + } + RecordSingleChannelId(false, aRequest); + return nsDocLoader::OnStopRequest(aRequest, aStatusCode); +} + +void nsDocShell::MaybeDisconnectChildListenersOnPageHide() { + MOZ_RELEASE_ASSERT(XRE_IsContentProcess()); + + if (mChannelToDisconnectOnPageHide != 0 && mLoadGroup) { + nsCOMPtr<nsISimpleEnumerator> requests; + mLoadGroup->GetRequests(getter_AddRefs(requests)); + for (const auto& request : SimpleEnumerator<nsIRequest>(requests)) { + RefPtr<DocumentChannel> channel = do_QueryObject(request); + if (channel && channel->ChannelId() == mChannelToDisconnectOnPageHide) { + static_cast<DocumentChannelChild*>(channel.get()) + ->DisconnectChildListeners(NS_BINDING_ABORTED, NS_BINDING_ABORTED); + } + } + mChannelToDisconnectOnPageHide = 0; + } +} diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h new file mode 100644 index 0000000000..67090146dc --- /dev/null +++ b/docshell/base/nsDocShell.h @@ -0,0 +1,1388 @@ +/* -*- 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/. */ + +#ifndef nsDocShell_h__ +#define nsDocShell_h__ + +#include "Units.h" +#include "mozilla/Encoding.h" +#include "mozilla/Maybe.h" +#include "mozilla/NotNull.h" +#include "mozilla/ScrollbarPreferences.h" +#include "mozilla/TimelineConsumers.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "nsCOMPtr.h" +#include "nsCharsetSource.h" +#include "nsDocLoader.h" +#include "nsIAuthPromptProvider.h" +#include "nsIBaseWindow.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIInterfaceRequestor.h" +#include "nsILoadContext.h" +#include "nsINetworkInterceptController.h" +#include "nsIRefreshURI.h" +#include "nsIWebNavigation.h" +#include "nsIWebPageDescriptor.h" +#include "nsIWebProgressListener.h" +#include "nsPoint.h" // mCurrent/mDefaultScrollbarPreferences +#include "nsRect.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "prtime.h" + +// Interfaces Needed + +namespace mozilla { +class Encoding; +class HTMLEditor; +class ObservedDocShell; +enum class TaskCategory; +namespace dom { +class ClientInfo; +class ClientSource; +class EventTarget; +class SessionHistoryInfo; +struct LoadingSessionHistoryInfo; +struct Wireframe; +} // namespace dom +namespace net { +class LoadInfo; +class DocumentLoadListener; +} // namespace net +} // namespace mozilla + +class nsIContentViewer; +class nsIController; +class nsIDocShellTreeOwner; +class nsIHttpChannel; +class nsIMutableArray; +class nsIPrompt; +class nsIScrollableFrame; +class nsIStringBundle; +class nsIURIFixup; +class nsIURIFixupInfo; +class nsIURILoader; +class nsIWebBrowserFind; +class nsIWidget; +class nsIReferrerInfo; + +class nsCommandManager; +class nsDocShellEditorData; +class nsDOMNavigationTiming; +class nsDSURIContentListener; +class nsGlobalWindowOuter; + +class FramingChecker; +class OnLinkClickEvent; + +/* internally used ViewMode types */ +enum ViewMode { viewNormal = 0x0, viewSource = 0x1 }; + +enum eCharsetReloadState { + eCharsetReloadInit, + eCharsetReloadRequested, + eCharsetReloadStopOrigional +}; + +class nsDocShell final : public nsDocLoader, + public nsIDocShell, + public nsIWebNavigation, + public nsIBaseWindow, + public nsIRefreshURI, + public nsIWebProgressListener, + public nsIWebPageDescriptor, + public nsIAuthPromptProvider, + public nsILoadContext, + public nsINetworkInterceptController, + public mozilla::SupportsWeakPtr { + public: + enum InternalLoad : uint32_t { + INTERNAL_LOAD_FLAGS_NONE = 0x0, + INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL = 0x1, + INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER = 0x2, + INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP = 0x4, + + // This flag marks the first load in this object + // @see nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD + INTERNAL_LOAD_FLAGS_FIRST_LOAD = 0x8, + + // The set of flags that should not be set before calling into + // nsDocShell::LoadURI and other nsDocShell loading functions. + INTERNAL_LOAD_FLAGS_LOADURI_SETUP_FLAGS = 0xf, + + INTERNAL_LOAD_FLAGS_BYPASS_CLASSIFIER = 0x10, + INTERNAL_LOAD_FLAGS_FORCE_ALLOW_COOKIES = 0x20, + + // Whether the load should be treated as srcdoc load, rather than a URI one. + INTERNAL_LOAD_FLAGS_IS_SRCDOC = 0x40, + + // Whether this is the load of a frame's original src attribute + INTERNAL_LOAD_FLAGS_ORIGINAL_FRAME_SRC = 0x80, + + INTERNAL_LOAD_FLAGS_NO_OPENER = 0x100, + + // Whether a top-level data URI navigation is allowed for that load + INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI = 0x200, + + // Whether the load should go through LoadURIDelegate. + INTERNAL_LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE = 0x2000, + }; + + // Event type dispatched by RestorePresentation + class RestorePresentationEvent : public mozilla::Runnable { + public: + NS_DECL_NSIRUNNABLE + explicit RestorePresentationEvent(nsDocShell* aDs) + : mozilla::Runnable("nsDocShell::RestorePresentationEvent"), + mDocShell(aDs) {} + void Revoke() { mDocShell = nullptr; } + + private: + RefPtr<nsDocShell> mDocShell; + }; + + class InterfaceRequestorProxy : public nsIInterfaceRequestor { + public: + explicit InterfaceRequestorProxy(nsIInterfaceRequestor* aRequestor); + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINTERFACEREQUESTOR + + private: + virtual ~InterfaceRequestorProxy(); + InterfaceRequestorProxy() = default; + nsWeakPtr mWeakPtr; + }; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsDocShell, nsDocLoader) + NS_DECL_NSIDOCSHELL + NS_DECL_NSIDOCSHELLTREEITEM + NS_DECL_NSIWEBNAVIGATION + NS_DECL_NSIBASEWINDOW + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIREFRESHURI + NS_DECL_NSIWEBPAGEDESCRIPTOR + NS_DECL_NSIAUTHPROMPTPROVIDER + NS_DECL_NSINETWORKINTERCEPTCONTROLLER + + // Create a new nsDocShell object. + static already_AddRefed<nsDocShell> Create( + mozilla::dom::BrowsingContext* aBrowsingContext, + uint64_t aContentWindowID = 0); + + bool Initialize(); + + NS_IMETHOD Stop() override { + // Need this here because otherwise nsIWebNavigation::Stop + // overrides the docloader's Stop() + return nsDocLoader::Stop(); + } + + mozilla::ScrollbarPreference ScrollbarPreference() const { + return mScrollbarPref; + } + void SetScrollbarPreference(mozilla::ScrollbarPreference); + + /* + * The size, in CSS pixels, of the margins for the <body> of an HTML document + * in this docshell; used to implement the marginwidth attribute on HTML + * <frame>/<iframe> elements. A value smaller than zero indicates that the + * attribute was not set. + */ + const mozilla::CSSIntSize& GetFrameMargins() const { return mFrameMargins; } + + bool UpdateFrameMargins(const mozilla::CSSIntSize& aMargins) { + if (mFrameMargins == aMargins) { + return false; + } + mFrameMargins = aMargins; + return true; + } + + /** + * Process a click on a link. + * + * @param aContent the content object used for triggering the link. + * @param aURI a URI object that defines the destination for the link + * @param aTargetSpec indicates where the link is targeted (may be an empty + * string) + * @param aFileName non-null when the link should be downloaded as the given + * file + * @param aPostDataStream the POST data to send + * @param aHeadersDataStream ??? (only used for plugins) + * @param aIsTrusted false if the triggerer is an untrusted DOM event. + * @param aTriggeringPrincipal, if not passed explicitly we fall back to + * the document's principal. + * @param aCsp, the CSP to be used for the load, that is the CSP of the + * entity responsible for causing the load to occur. Most likely + * this is the CSP of the document that started the load. In case + * aCsp was not passed explicitly we fall back to using + * aContent's document's CSP if that document holds any. + */ + nsresult OnLinkClick(nsIContent* aContent, nsIURI* aURI, + const nsAString& aTargetSpec, const nsAString& aFileName, + nsIInputStream* aPostDataStream, + nsIInputStream* aHeadersDataStream, + bool aIsUserTriggered, bool aIsTrusted, + nsIPrincipal* aTriggeringPrincipal, + nsIContentSecurityPolicy* aCsp); + /** + * Process a click on a link. + * + * Works the same as OnLinkClick() except it happens immediately rather than + * through an event. + * + * @param aContent the content object used for triggering the link. + * @param aDocShellLoadState the extended load info for this load. + * @param aNoOpenerImplied if the link implies "noopener" + * @param aTriggeringPrincipal, if not passed explicitly we fall back to + * the document's principal. + */ + nsresult OnLinkClickSync(nsIContent* aContent, + nsDocShellLoadState* aLoadState, + bool aNoOpenerImplied, + nsIPrincipal* aTriggeringPrincipal); + + /** + * Process a mouse-over a link. + * + * @param aContent the linked content. + * @param aURI an URI object that defines the destination for the link + * @param aTargetSpec indicates where the link is targeted (it may be an empty + * string) + */ + nsresult OnOverLink(nsIContent* aContent, nsIURI* aURI, + const nsAString& aTargetSpec); + /** + * Process the mouse leaving a link. + */ + nsresult OnLeaveLink(); + + // Don't use NS_DECL_NSILOADCONTEXT because some of nsILoadContext's methods + // are shared with nsIDocShell and can't be declared twice. + NS_IMETHOD GetAssociatedWindow(mozIDOMWindowProxy**) override; + NS_IMETHOD GetTopWindow(mozIDOMWindowProxy**) override; + NS_IMETHOD GetTopFrameElement(mozilla::dom::Element**) override; + NS_IMETHOD GetIsContent(bool*) override; + NS_IMETHOD GetUsePrivateBrowsing(bool*) override; + NS_IMETHOD SetUsePrivateBrowsing(bool) override; + NS_IMETHOD SetPrivateBrowsing(bool) override; + NS_IMETHOD GetUseRemoteTabs(bool*) override; + NS_IMETHOD SetRemoteTabs(bool) override; + NS_IMETHOD GetUseRemoteSubframes(bool*) override; + NS_IMETHOD SetRemoteSubframes(bool) override; + NS_IMETHOD GetScriptableOriginAttributes( + JSContext*, JS::MutableHandle<JS::Value>) override; + NS_IMETHOD_(void) + GetOriginAttributes(mozilla::OriginAttributes& aAttrs) override; + + // Restores a cached presentation from history (mLSHE). + // This method swaps out the content viewer and simulates loads for + // subframes. It then simulates the completion of the toplevel load. + nsresult RestoreFromHistory(); + + /** + * Parses the passed in header string and sets up a refreshURI if a "refresh" + * header is found. If docshell is busy loading a page currently, the request + * will be queued and executed when the current page finishes loading. + * + * @param aDocument document to which the refresh header applies. + * @param aHeader The meta refresh header string. + */ + void SetupRefreshURIFromHeader(mozilla::dom::Document* aDocument, + const nsAString& aHeader); + + // Perform a URI load from a refresh timer. This is just like the + // ForceRefreshURI method on nsIRefreshURI, but makes sure to take + // the timer involved out of mRefreshURIList if it's there. + // aTimer must not be null. + nsresult ForceRefreshURIFromTimer(nsIURI* aURI, nsIPrincipal* aPrincipal, + uint32_t aDelay, nsITimer* aTimer); + + // We need dummy OnLocationChange in some cases to update the UI without + // updating security info. + void FireDummyOnLocationChange() { + FireOnLocationChange(this, nullptr, mCurrentURI, + LOCATION_CHANGE_SAME_DOCUMENT); + } + + // This function is created exclusively for dom.background_loading_iframe is + // set. As soon as the current DocShell knows itself can be treated as + // background loading, it triggers the parent docshell to see if the parent + // document can fire load event earlier. + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void TriggerParentCheckDocShellIsEmpty(); + + nsresult HistoryEntryRemoved(int32_t aIndex); + + // Notify Scroll observers when an async panning/zooming transform + // has started being applied + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void NotifyAsyncPanZoomStarted(); + + // Notify Scroll observers when an async panning/zooming transform + // is no longer applied + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void NotifyAsyncPanZoomStopped(); + + void SetInFrameSwap(bool aInSwap) { mInFrameSwap = aInSwap; } + bool InFrameSwap(); + + bool GetForcedAutodetection() { return mForcedAutodetection; } + + void ResetForcedAutodetection() { mForcedAutodetection = false; } + + mozilla::HTMLEditor* GetHTMLEditorInternal(); + nsresult SetHTMLEditorInternal(mozilla::HTMLEditor* aHTMLEditor); + + // Handle page navigation due to charset changes + nsresult CharsetChangeReloadDocument( + mozilla::NotNull<const mozilla::Encoding*> aEncoding, int32_t aSource); + nsresult CharsetChangeStopDocumentLoad(); + + nsDOMNavigationTiming* GetNavigationTiming() const; + + nsresult SetOriginAttributes(const mozilla::OriginAttributes& aAttrs); + + const mozilla::OriginAttributes& GetOriginAttributes() { + return mBrowsingContext->OriginAttributesRef(); + } + + // Determine whether this docshell corresponds to the given history entry, + // via having a pointer to it in mOSHE or mLSHE. + bool HasHistoryEntry(nsISHEntry* aEntry) const { + return aEntry && (aEntry == mOSHE || aEntry == mLSHE); + } + + // Update any pointers (mOSHE or mLSHE) to aOldEntry to point to aNewEntry + void SwapHistoryEntries(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry); + + bool GetCreatedDynamically() const { + return mBrowsingContext && mBrowsingContext->CreatedDynamically(); + } + + mozilla::gfx::Matrix5x4* GetColorMatrix() { return mColorMatrix.get(); } + + static bool SandboxFlagsImplyCookies(const uint32_t& aSandboxFlags); + + // Tell the favicon service that aNewURI has the same favicon as aOldURI. + static void CopyFavicon(nsIURI* aOldURI, nsIURI* aNewURI, + bool aInPrivateBrowsing); + + static nsDocShell* Cast(nsIDocShell* aDocShell) { + return static_cast<nsDocShell*>(aDocShell); + } + + static bool CanLoadInParentProcess(nsIURI* aURI); + + // Returns true if the current load is a force reload (started by holding + // shift while triggering reload) + bool IsForceReloading(); + + mozilla::dom::WindowProxyHolder GetWindowProxy() { + EnsureScriptEnvironment(); + return mozilla::dom::WindowProxyHolder(mBrowsingContext); + } + + /** + * Loads the given URI. See comments on nsDocShellLoadState members for more + * information on information used. + * `aCacheKey` gets passed to DoURILoad call. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult InternalLoad( + nsDocShellLoadState* aLoadState, + mozilla::Maybe<uint32_t> aCacheKey = mozilla::Nothing()); + + void MaybeRestoreWindowName(); + + void StoreWindowNameToSHEntries(); + + void SetWillChangeProcess() { mWillChangeProcess = true; } + bool WillChangeProcess() { return mWillChangeProcess; } + + // Create a content viewer within this nsDocShell for the given + // `WindowGlobalChild` actor. + nsresult CreateContentViewerForActor( + mozilla::dom::WindowGlobalChild* aWindowActor); + + // Creates a real network channel (not a DocumentChannel) using the specified + // parameters. + // Used by nsDocShell when not using DocumentChannel, by DocumentLoadListener + // (parent-process DocumentChannel), and by DocumentChannelChild/ContentChild + // to transfer the resulting channel into the final process. + static nsresult CreateRealChannelForDocument( + nsIChannel** aChannel, nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIInterfaceRequestor* aCallbacks, nsLoadFlags aLoadFlags, + const nsAString& aSrcdoc, nsIURI* aBaseURI); + + // Creates a real (not DocumentChannel) channel, and configures it using the + // supplied nsDocShellLoadState. + // Configuration options here are ones that should be applied to only the + // real channel, especially ones that need to QI to channel subclasses. + static bool CreateAndConfigureRealChannelForLoadState( + mozilla::dom::BrowsingContext* aBrowsingContext, + nsDocShellLoadState* aLoadState, mozilla::net::LoadInfo* aLoadInfo, + nsIInterfaceRequestor* aCallbacks, nsDocShell* aDocShell, + const mozilla::OriginAttributes& aOriginAttributes, + nsLoadFlags aLoadFlags, uint32_t aCacheKey, nsresult& rv, + nsIChannel** aChannel); + + // This is used to deal with errors resulting from a failed page load. + // Errors are handled as follows: + // 1. Check to see if it's a file not found error or bad content + // encoding error. + // 2. Send the URI to a keyword server (if enabled) + // 3. If the error was DNS failure, then add www and .com to the URI + // (if appropriate). + // 4. If the www .com additions don't work, try those with an HTTPS scheme + // (if appropriate). + static already_AddRefed<nsIURI> AttemptURIFixup( + nsIChannel* aChannel, nsresult aStatus, + const mozilla::Maybe<nsCString>& aOriginalURIString, uint32_t aLoadType, + bool aIsTopFrame, bool aAllowKeywordFixup, bool aUsePrivateBrowsing, + bool aNotifyKeywordSearchLoading = false, + nsIInputStream** aNewPostData = nullptr); + + static already_AddRefed<nsIURI> MaybeFixBadCertDomainErrorURI( + nsIChannel* aChannel, nsIURI* aUrl); + + // Takes aStatus and filters out results that should not display + // an error page. + // If this returns a failed result, then we should display an error + // page with that result. + // aSkippedUnknownProtocolNavigation will be set to true if we chose + // to skip displaying an error page for an NS_ERROR_UNKNOWN_PROTOCOL + // navigation. + static nsresult FilterStatusForErrorPage( + nsresult aStatus, nsIChannel* aChannel, uint32_t aLoadType, + bool aIsTopFrame, bool aUseErrorPages, bool aIsInitialDocument, + bool* aSkippedUnknownProtocolNavigation = nullptr); + + // Notify consumers of a search being loaded through the observer service: + static void MaybeNotifyKeywordSearchLoading(const nsString& aProvider, + const nsString& aKeyword); + + nsDocShell* GetInProcessChildAt(int32_t aIndex); + + static bool ShouldAddURIVisit(nsIChannel* aChannel); + + /** + * Helper function that finds the last URI and its transition flags for a + * channel. + * + * This method first checks the channel's property bag to see if previous + * info has been saved. If not, it gives back the referrer of the channel. + * + * @param aChannel + * The channel we are transitioning to + * @param aURI + * Output parameter with the previous URI, not addref'd + * @param aChannelRedirectFlags + * If a redirect, output parameter with the previous redirect flags + * from nsIChannelEventSink + */ + static void ExtractLastVisit(nsIChannel* aChannel, nsIURI** aURI, + uint32_t* aChannelRedirectFlags); + + bool HasContentViewer() const { return !!mContentViewer; } + + static uint32_t ComputeURILoaderFlags( + mozilla::dom::BrowsingContext* aBrowsingContext, uint32_t aLoadType); + + void SetLoadingSessionHistoryInfo( + const mozilla::dom::LoadingSessionHistoryInfo& aLoadingInfo, + bool aNeedToReportActiveAfterLoadingBecomesActive = false); + const mozilla::dom::LoadingSessionHistoryInfo* + GetLoadingSessionHistoryInfo() { + return mLoadingEntry.get(); + } + + already_AddRefed<nsIInputStream> GetPostDataFromCurrentEntry() const; + mozilla::Maybe<uint32_t> GetCacheKeyFromCurrentEntry() const; + + // Loading and/or active entries are only set when session history + // in the parent is on. + bool FillLoadStateFromCurrentEntry(nsDocShellLoadState& aLoadState); + + static bool ShouldAddToSessionHistory(nsIURI* aURI, nsIChannel* aChannel); + + bool IsOSHE(nsISHEntry* aEntry) const { return mOSHE == aEntry; } + + mozilla::dom::ChildSHistory* GetSessionHistory() { + return mBrowsingContext->GetChildSessionHistory(); + } + + // This returns true only when using session history in parent. + bool IsLoadingFromSessionHistory(); + + NS_IMETHODIMP OnStartRequest(nsIRequest* aRequest) override; + NS_IMETHODIMP OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) override; + + private: // member functions + friend class nsAppShellService; + friend class nsDSURIContentListener; + friend class FramingChecker; + friend class OnLinkClickEvent; + friend class nsIDocShell; + friend class mozilla::dom::BrowsingContext; + friend class mozilla::net::DocumentLoadListener; + friend class nsGlobalWindowOuter; + + // It is necessary to allow adding a timeline marker wherever a docshell + // instance is available. This operation happens frequently and needs to + // be very fast, so instead of using a Map or having to search for some + // docshell-specific markers storage, a pointer to an `ObservedDocShell` is + // is stored on docshells directly. + friend void mozilla::TimelineConsumers::AddConsumer(nsDocShell*); + friend void mozilla::TimelineConsumers::RemoveConsumer(nsDocShell*); + friend void mozilla::TimelineConsumers::AddMarkerForDocShell( + nsDocShell*, const char*, MarkerTracingType, MarkerStackRequest); + friend void mozilla::TimelineConsumers::AddMarkerForDocShell( + nsDocShell*, const char*, const TimeStamp&, MarkerTracingType, + MarkerStackRequest); + friend void mozilla::TimelineConsumers::AddMarkerForDocShell( + nsDocShell*, UniquePtr<AbstractTimelineMarker>&&); + friend void mozilla::TimelineConsumers::PopMarkers( + nsDocShell*, JSContext*, nsTArray<dom::ProfileTimelineMarker>&); + + nsDocShell(mozilla::dom::BrowsingContext* aBrowsingContext, + uint64_t aContentWindowID); + + static inline uint32_t PRTimeToSeconds(PRTime aTimeUsec) { + return uint32_t(aTimeUsec / PR_USEC_PER_SEC); + } + + virtual ~nsDocShell(); + + // + // nsDocLoader + // + + virtual void DestroyChildren() override; + + // Overridden from nsDocLoader, this provides more information than the + // normal OnStateChange with flags STATE_REDIRECTING + virtual void OnRedirectStateChange(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, + uint32_t aRedirectFlags, + uint32_t aStateFlags) override; + + // Override the parent setter from nsDocLoader + virtual nsresult SetDocLoaderParent(nsDocLoader* aLoader) override; + + // + // Content Viewer Management + // + + nsresult EnsureContentViewer(); + + // aPrincipal can be passed in if the caller wants. If null is + // passed in, the about:blank principal will end up being used. + // aCSP, if any, will be used for the new about:blank load. + nsresult CreateAboutBlankContentViewer( + nsIPrincipal* aPrincipal, nsIPrincipal* aPartitionedPrincipal, + nsIContentSecurityPolicy* aCSP, nsIURI* aBaseURI, bool aIsInitialDocument, + const mozilla::Maybe<nsILoadInfo::CrossOriginEmbedderPolicy>& aCOEP = + mozilla::Nothing(), + bool aTryToSaveOldPresentation = true, bool aCheckPermitUnload = true, + mozilla::dom::WindowGlobalChild* aActor = nullptr); + + nsresult CreateContentViewer(const nsACString& aContentType, + nsIRequest* aRequest, + nsIStreamListener** aContentHandler); + + nsresult NewContentViewerObj(const nsACString& aContentType, + nsIRequest* aRequest, nsILoadGroup* aLoadGroup, + nsIStreamListener** aContentHandler, + nsIContentViewer** aViewer); + + already_AddRefed<nsILoadURIDelegate> GetLoadURIDelegate(); + + nsresult SetupNewViewer( + nsIContentViewer* aNewViewer, + mozilla::dom::WindowGlobalChild* aWindowActor = nullptr); + + // + // Session History + // + + // Either aChannel or aOwner must be null. If aChannel is + // present, the owner should be gotten from it. + // If aCloneChildren is true, then our current session history's + // children will be cloned onto the new entry. This should be + // used when we aren't actually changing the document while adding + // the new session history entry. + // aCsp is the CSP to be used for the load. That is *not* the CSP + // that will be applied to subresource loads within that document + // but the CSP for the document load itself. E.g. if that CSP + // includes upgrade-insecure-requests, then the new top-level load + // will be upgraded to HTTPS. + nsresult AddToSessionHistory(nsIURI* aURI, nsIChannel* aChannel, + nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, + bool aCloneChildren, nsISHEntry** aNewEntry); + + void UpdateActiveEntry( + bool aReplace, const mozilla::Maybe<nsPoint>& aPreviousScrollPos, + nsIURI* aURI, nsIURI* aOriginalURI, nsIReferrerInfo* aReferrerInfo, + nsIPrincipal* aTriggeringPrincipal, nsIContentSecurityPolicy* aCsp, + const nsAString& aTitle, bool aScrollRestorationIsManual, + nsIStructuredCloneContainer* aData, bool aURIWasModified); + + nsresult AddChildSHEntry(nsISHEntry* aCloneRef, nsISHEntry* aNewEntry, + int32_t aChildOffset, uint32_t aLoadType, + bool aCloneChildren); + + nsresult AddChildSHEntryToParent(nsISHEntry* aNewEntry, int32_t aChildOffset, + bool aCloneChildren); + + // Call this method to swap in a new history entry to m[OL]SHE, rather than + // setting it directly. This completes the navigation in all docshells + // in the case of a subframe navigation. + // Returns old mOSHE/mLSHE. + already_AddRefed<nsISHEntry> SetHistoryEntry(nsCOMPtr<nsISHEntry>* aPtr, + nsISHEntry* aEntry); + + // This method calls SetHistoryEntry and updates mOSHE and mLSHE in BC to be + // the same as in docshell + void SetHistoryEntryAndUpdateBC(const mozilla::Maybe<nsISHEntry*>& aLSHE, + const mozilla::Maybe<nsISHEntry*>& aOSHE); + + // If aNotifiedBeforeUnloadListeners is true, "beforeunload" event listeners + // were notified by the caller and given the chance to abort the navigation, + // and should not be notified again. + static nsresult ReloadDocument( + nsDocShell* aDocShell, mozilla::dom::Document* aDocument, + uint32_t aLoadType, mozilla::dom::BrowsingContext* aBrowsingContext, + nsIURI* aCurrentURI, nsIReferrerInfo* aReferrerInfo, + bool aNotifiedBeforeUnloadListeners = false); + + public: + bool IsAboutBlankLoadOntoInitialAboutBlank(nsIURI* aURI, + bool aInheritPrincipal, + nsIPrincipal* aPrincipalToInherit); + + private: + // + // URI Load + // + + // Actually open a channel and perform a URI load. Callers need to pass a + // non-null aLoadState->TriggeringPrincipal() which initiated the URI load. + // Please note that the TriggeringPrincipal will be used for performing + // security checks. If aLoadState->URI() is provided by the web, then please + // do not pass a SystemPrincipal as the triggeringPrincipal. If + // aLoadState()->PrincipalToInherit is null, then no inheritance of any sort + // will happen and the load will get a principal based on the URI being + // loaded. If the Srcdoc flag is set (INTERNAL_LOAD_FLAGS_IS_SRCDOC), the load + // will be considered as a srcdoc load, and the contents of Srcdoc will be + // loaded instead of the URI. aLoadState->OriginalURI() will be set as the + // originalURI on the channel that does the load. If OriginalURI is null, URI + // will be set as the originalURI. If LoadReplace is true, LOAD_REPLACE flag + // will be set on the nsIChannel. + // If `aCacheKey` is supplied, use it for the session history entry. + nsresult DoURILoad(nsDocShellLoadState* aLoadState, + mozilla::Maybe<uint32_t> aCacheKey, nsIRequest** aRequest); + + static nsresult AddHeadersToChannel(nsIInputStream* aHeadersData, + nsIChannel* aChannel); + + nsresult OpenInitializedChannel(nsIChannel* aChannel, + nsIURILoader* aURILoader, + uint32_t aOpenFlags); + nsresult OpenRedirectedChannel(nsDocShellLoadState* aLoadState); + + void UpdateMixedContentChannelForNewLoad(nsIChannel* aChannel); + + MOZ_CAN_RUN_SCRIPT + nsresult ScrollToAnchor(bool aCurHasRef, bool aNewHasRef, + nsACString& aNewHash, uint32_t aLoadType); + + private: + // Returns true if would have called FireOnLocationChange, + // but did not because aFireOnLocationChange was false on entry. + // In this case it is the caller's responsibility to ensure + // FireOnLocationChange is called. + // In all other cases false is returned. + // Either aChannel or aTriggeringPrincipal must be null. If aChannel is + // present, the owner should be gotten from it. + // If OnNewURI calls AddToSessionHistory, it will pass its + // aCloneSHChildren argument as aCloneChildren. + // aCsp is the CSP to be used for the load. That is *not* the CSP + // that will be applied to subresource loads within that document + // but the CSP for the document load itself. E.g. if that CSP + // includes upgrade-insecure-requests, then the new top-level load + // will be upgraded to HTTPS. + bool OnNewURI(nsIURI* aURI, nsIChannel* aChannel, + nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInehrit, + nsIContentSecurityPolicy* aCsp, bool aFireOnLocationChange, + bool aAddToGlobalHistory, bool aCloneSHChildren); + + public: + // If wireframe collection is enabled, will attempt to gather the + // wireframe for the document. + mozilla::Maybe<mozilla::dom::Wireframe> GetWireframe(); + + // If wireframe collection is enabled, will attempt to gather the + // wireframe for the document and stash it inside of the active history + // entry. Returns true if wireframes were collected. + bool CollectWireframe(); + + // Helper method that is called when a new document (including any + // sub-documents - ie. frames) has been completely loaded. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult EndPageLoad(nsIWebProgress* aProgress, nsIChannel* aChannel, + nsresult aResult); + + // Builds an error page URI (e.g. about:neterror?etc) for the given aURI + // and displays it via the LoadErrorPage() overload below. + nsresult LoadErrorPage(nsIURI* aURI, const char16_t* aURL, + const char* aErrorPage, const char* aErrorType, + const char16_t* aDescription, const char* aCSSClass, + nsIChannel* aFailedChannel); + + // This method directly loads aErrorURI as an error page. aFailedURI and + // aFailedChannel come from DisplayLoadError() or the LoadErrorPage() overload + // above. + nsresult LoadErrorPage(nsIURI* aErrorURI, nsIURI* aFailedURI, + nsIChannel* aFailedChannel); + + bool DisplayLoadError(nsresult aError, nsIURI* aURI, const char16_t* aURL, + nsIChannel* aFailedChannel) { + bool didDisplayLoadError = false; + DisplayLoadError(aError, aURI, aURL, aFailedChannel, &didDisplayLoadError); + return didDisplayLoadError; + } + + // + // Uncategorized + // + + // Get the principal that we'll set on the channel if we're inheriting. If + // aConsiderCurrentDocument is true, we try to use the current document if + // at all possible. If that fails, we fall back on the parent document. + // If that fails too, we force creation of a content viewer and use the + // resulting principal. If aConsiderCurrentDocument is false, we just look + // at the parent. + // If aConsiderPartitionedPrincipal is true, we consider the partitioned + // principal instead of the node principal. + nsIPrincipal* GetInheritedPrincipal( + bool aConsiderCurrentDocument, + bool aConsiderPartitionedPrincipal = false); + + /** + * Helper function that caches a URI and a transition for saving later. + * + * @param aChannel + * Channel that will have these properties saved + * @param aURI + * The URI to save for later + * @param aChannelRedirectFlags + * The nsIChannelEventSink redirect flags to save for later + */ + static void SaveLastVisit(nsIChannel* aChannel, nsIURI* aURI, + uint32_t aChannelRedirectFlags); + + /** + * Helper function for adding a URI visit using IHistory. + * + * The IHistory API maintains chains of visits, tracking both HTTP referrers + * and redirects for a user session. VisitURI requires the current URI and + * the previous URI in the chain. + * + * Visits can be saved either during a redirect or when the request has + * reached its final destination. The previous URI in the visit may be + * from another redirect. + * + * @pre aURI is not null. + * + * @param aURI + * The URI that was just visited + * @param aPreviousURI + * The previous URI of this visit + * @param aChannelRedirectFlags + * For redirects, the redirect flags from nsIChannelEventSink + * (0 otherwise) + * @param aResponseStatus + * For HTTP channels, the response code (0 otherwise). + */ + void AddURIVisit(nsIURI* aURI, nsIURI* aPreviousURI, + uint32_t aChannelRedirectFlags, + uint32_t aResponseStatus = 0); + + /** + * Internal helper funtion + */ + static void InternalAddURIVisit( + nsIURI* aURI, nsIURI* aPreviousURI, uint32_t aChannelRedirectFlags, + uint32_t aResponseStatus, mozilla::dom::BrowsingContext* aBrowsingContext, + nsIWidget* aWidget, uint32_t aLoadType); + + static already_AddRefed<nsIURIFixupInfo> KeywordToURI( + const nsACString& aKeyword, bool aIsPrivateContext); + + // Sets the current document's current state object to the given SHEntry's + // state object. The current state object is eventually given to the page + // in the PopState event. + void SetDocCurrentStateObj(nsISHEntry* aShEntry, + mozilla::dom::SessionHistoryInfo* aInfo); + + // Returns true if would have called FireOnLocationChange, + // but did not because aFireOnLocationChange was false on entry. + // In this case it is the caller's responsibility to ensure + // FireOnLocationChange is called. + // In all other cases false is returned. + bool SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest, + bool aFireOnLocationChange, bool aIsInitialAboutBlank, + uint32_t aLocationFlags); + + // The following methods deal with saving and restoring content viewers + // in session history. + + // mContentViewer points to the current content viewer associated with + // this docshell. When loading a new document, the content viewer is + // either destroyed or stored into a session history entry. To make sure + // that destruction happens in a controlled fashion, a given content viewer + // is always owned in exactly one of these ways: + // 1) The content viewer is active and owned by a docshell's + // mContentViewer. + // 2) The content viewer is still being displayed while we begin loading + // a new document. The content viewer is owned by the _new_ + // content viewer's mPreviousViewer, and has a pointer to the + // nsISHEntry where it will eventually be stored. The content viewer + // has been close()d by the docshell, which detaches the document from + // the window object. + // 3) The content viewer is cached in session history. The nsISHEntry + // has the only owning reference to the content viewer. The viewer + // has released its nsISHEntry pointer to prevent circular ownership. + // + // When restoring a content viewer from session history, open() is called + // to reattach the document to the window object. The content viewer is + // then placed into mContentViewer and removed from the history entry. + // (mContentViewer is put into session history as described above, if + // applicable). + + // Determines whether we can safely cache the current mContentViewer in + // session history. This checks a number of factors such as cache policy, + // pending requests, and unload handlers. + // |aLoadType| should be the load type that will replace the current + // presentation. |aNewRequest| should be the request for the document to + // be loaded in place of the current document, or null if such a request + // has not been created yet. |aNewDocument| should be the document that will + // replace the current document. + bool CanSavePresentation(uint32_t aLoadType, nsIRequest* aNewRequest, + mozilla::dom::Document* aNewDocument, + bool aReportBFCacheComboTelemetry); + + static void ReportBFCacheComboTelemetry(uint32_t aCombo); + + // Captures the state of the supporting elements of the presentation + // (the "window" object, docshell tree, meta-refresh loads, and security + // state) and stores them on |mOSHE|. + nsresult CaptureState(); + + // Begin the toplevel restore process for |aSHEntry|. + // This simulates a channel open, and defers the real work until + // RestoreFromHistory is called from a PLEvent. + nsresult RestorePresentation(nsISHEntry* aSHEntry, bool* aRestoring); + + // Call BeginRestore(nullptr, false) for each child of this shell. + nsresult BeginRestoreChildren(); + + // Method to get our current position and size without flushing + void DoGetPositionAndSize(int32_t* aX, int32_t* aY, int32_t* aWidth, + int32_t* aHeight); + + // Call this when a URI load is handed to us (via OnLinkClick or + // InternalLoad). This makes sure that we're not inside unload, or that if + // we are it's still OK to load this URI. + bool IsOKToLoadURI(nsIURI* aURI); + + // helpers for executing commands + nsresult GetControllerForCommand(const char* aCommand, + nsIController** aResult); + + // Possibly create a ClientSource object to represent an initial about:blank + // window that has not been allocated yet. Normally we try not to create + // this about:blank window until something calls GetDocument(). We still need + // the ClientSource to exist for this conceptual window, though. + // + // The ClientSource is created with the given principal if specified. If + // the principal is not provided we will attempt to inherit it when we + // are sure it will match what the real about:blank window principal + // would have been. There are some corner cases where we cannot easily + // determine the correct principal and will not create the ClientSource. + // In these cases the initial about:blank will appear to not exist until + // its real document and window are created. + void MaybeCreateInitialClientSource(nsIPrincipal* aPrincipal = nullptr); + + // Determine if a service worker is allowed to control a window in this + // docshell with the given URL. If there are any reasons it should not, + // this will return false. If true is returned then the window *may* be + // controlled. The caller must still consult either the parent controller + // or the ServiceWorkerManager to determine if a service worker should + // actually control the window. + bool ServiceWorkerAllowedToControlWindow(nsIPrincipal* aPrincipal, + nsIURI* aURI); + + // Return the ClientInfo for the initial about:blank window, if it exists + // or we have speculatively created a ClientSource in + // MaybeCreateInitialClientSource(). This can return a ClientInfo object + // even if GetExtantDoc() returns nullptr. + mozilla::Maybe<mozilla::dom::ClientInfo> GetInitialClientInfo() const; + + /** + * Initializes mTiming if it isn't yet. + * After calling this, mTiming is non-null. This method returns true if the + * initialization of the Timing can be reset (basically this is true if a new + * Timing object is created). + * In case the loading is aborted, MaybeResetInitTiming() can be called + * passing the return value of MaybeInitTiming(): if it's possible to reset + * the Timing, this method will do it. + */ + [[nodiscard]] bool MaybeInitTiming(); + void MaybeResetInitTiming(bool aReset); + + // Convenience method for getting our parent docshell. Can return null + already_AddRefed<nsDocShell> GetInProcessParentDocshell(); + + // Internal implementation of nsIDocShell::FirePageHideNotification. + // If aSkipCheckingDynEntries is true, it will not try to remove dynamic + // subframe entries. This is to avoid redundant RemoveDynEntries calls in all + // children docshells. + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void FirePageHideNotificationInternal( + bool aIsUnload, bool aSkipCheckingDynEntries); + + void ThawFreezeNonRecursive(bool aThaw); + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void FirePageHideShowNonRecursive(bool aShow); + + nsresult Dispatch(mozilla::TaskCategory aCategory, + already_AddRefed<nsIRunnable>&& aRunnable); + + void ReattachEditorToWindow(nsISHEntry* aSHEntry); + void ClearFrameHistory(nsISHEntry* aEntry); + // Determine if this type of load should update history. + static bool ShouldUpdateGlobalHistory(uint32_t aLoadType); + void UpdateGlobalHistoryTitle(nsIURI* aURI); + bool IsSubframe() { return mBrowsingContext->IsSubframe(); } + bool CanSetOriginAttributes(); + bool ShouldBlockLoadingForBackButton(); + static bool ShouldDiscardLayoutState(nsIHttpChannel* aChannel); + bool HasUnloadedParent(); + bool JustStartedNetworkLoad(); + bool NavigationBlockedByPrinting(bool aDisplayErrorDialog = true); + bool IsNavigationAllowed(bool aDisplayPrintErrorDialog = true, + bool aCheckIfUnloadFired = true); + nsIScrollableFrame* GetRootScrollFrame(); + nsIChannel* GetCurrentDocChannel(); + nsresult EnsureScriptEnvironment(); + nsresult EnsureEditorData(); + nsresult EnsureTransferableHookData(); + nsresult EnsureFind(); + nsresult EnsureCommandHandler(); + nsresult RefreshURIFromQueue(); + void RefreshURIToQueue(); + nsresult Embed(nsIContentViewer* aContentViewer, + mozilla::dom::WindowGlobalChild* aWindowActor, + bool aIsTransientAboutBlank, bool aPersist, + nsIRequest* aRequest, nsIURI* aPreviousURI); + nsPresContext* GetEldestPresContext(); + nsresult CheckLoadingPermissions(); + nsresult LoadHistoryEntry(nsISHEntry* aEntry, uint32_t aLoadType, + bool aUserActivation); + nsresult LoadHistoryEntry( + const mozilla::dom::LoadingSessionHistoryInfo& aEntry, uint32_t aLoadType, + bool aUserActivation); + nsresult LoadHistoryEntry(nsDocShellLoadState* aLoadState, uint32_t aLoadType, + bool aLoadingCurrentEntry); + nsresult GetHttpChannel(nsIChannel* aChannel, nsIHttpChannel** aReturn); + nsresult ConfirmRepost(bool* aRepost); + nsresult GetPromptAndStringBundle(nsIPrompt** aPrompt, + nsIStringBundle** aStringBundle); + nsresult SetCurScrollPosEx(int32_t aCurHorizontalPos, + int32_t aCurVerticalPos); + nsPoint GetCurScrollPos(); + + already_AddRefed<mozilla::dom::ChildSHistory> GetRootSessionHistory(); + + bool CSSErrorReportingEnabled() const { return mCSSErrorReportingEnabled; } + + // Handles retrieval of subframe session history for nsDocShell::LoadURI. If a + // load is requested in a subframe of the current DocShell, the subframe + // loadType may need to reflect the loadType of the parent document, or in + // some cases (like reloads), the history load may need to be cancelled. See + // function comments for in-depth logic descriptions. + // Returns true if the method itself deals with the load. + bool MaybeHandleSubframeHistory(nsDocShellLoadState* aLoadState, + bool aContinueHandlingSubframeHistory); + + // If we are passed a named target during InternalLoad, this method handles + // moving the load to the browsing context the target name resolves to. + nsresult PerformRetargeting(nsDocShellLoadState* aLoadState); + + // Returns one of nsIContentPolicy::TYPE_DOCUMENT, + // nsIContentPolicy::TYPE_INTERNAL_IFRAME, or + // nsIContentPolicy::TYPE_INTERNAL_FRAME depending on who is responsible for + // this docshell. + nsContentPolicyType DetermineContentType(); + + // If this is an iframe, and the embedder is OOP, then notifes the + // embedder that loading has finished and we shouldn't be blocking + // load of the embedder. Only called when we fail to load, as we wait + // for the load event of our Document before notifying success. + // + // If aFireFrameErrorEvent is true, then fires an error event at the + // embedder element, for both in-process and OOP embedders. + void UnblockEmbedderLoadEventForFailure(bool aFireFrameErrorEvent = false); + + struct SameDocumentNavigationState { + nsAutoCString mCurrentHash; + nsAutoCString mNewHash; + bool mCurrentURIHasRef = false; + bool mNewURIHasRef = false; + bool mSameExceptHashes = false; + bool mSecureUpgradeURI = false; + bool mHistoryNavBetweenSameDoc = false; + }; + + // Check to see if we're loading a prior history entry or doing a fragment + // navigation in the same document. + // NOTE: In case we are doing a fragment navigation, and HTTPS-Only/ -First + // mode is enabled and upgraded the underlying document, we update the URI of + // aLoadState from HTTP to HTTPS (if neccessary). + bool IsSameDocumentNavigation(nsDocShellLoadState* aLoadState, + SameDocumentNavigationState& aState); + + // ... If so, handle the scrolling or other action required instead of + // continuing with new document navigation. + MOZ_CAN_RUN_SCRIPT + nsresult HandleSameDocumentNavigation(nsDocShellLoadState* aLoadState, + SameDocumentNavigationState& aState); + + uint32_t GetSameDocumentNavigationFlags(nsIURI* aNewURI); + + // Called when the Private Browsing state of a nsDocShell changes. + void NotifyPrivateBrowsingChanged(); + + // Internal helpers for BrowsingContext to pass update values to nsIDocShell's + // LoadGroup. + void SetLoadGroupDefaultLoadFlags(nsLoadFlags aLoadFlags); + + void SetTitleOnHistoryEntry(bool aUpdateEntryInSessionHistory); + + void SetScrollRestorationIsManualOnHistoryEntry(nsISHEntry* aSHEntry, + bool aIsManual); + + void SetCacheKeyOnHistoryEntry(nsISHEntry* aSHEntry, uint32_t aCacheKey); + + // If the LoadState's URI is a javascript: URI, checks that the triggering + // principal subsumes the principal of the current document, and returns + // NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI if it does not. + nsresult CheckDisallowedJavascriptLoad(nsDocShellLoadState* aLoadState); + + nsresult LoadURI(nsDocShellLoadState* aLoadState, bool aSetNavigating, + bool aContinueHandlingSubframeHistory); + + // Sets the active entry to the current loading entry. aPersist is used in the + // case a new session history entry is added to the session history. + // aExpired is true if the relevant nsIChannel has its cache token expired. + // aCacheKey is the channel's cache key. + // aPreviousURI should be the URI that was previously loaded into the + // nsDocshell + void MoveLoadingToActiveEntry(bool aPersist, bool aExpired, + uint32_t aCacheKey, nsIURI* aPreviousURI); + + void ActivenessMaybeChanged(); + + /** + * Returns true if `noopener` will be force-enabled by any attempt to create + * a popup window, even if rel="opener" is requested. + */ + bool NoopenerForceEnabled(); + + bool ShouldOpenInBlankTarget(const nsAString& aOriginalTarget, + nsIURI* aLinkURI, nsIContent* aContent, + bool aIsUserTriggered); + + void RecordSingleChannelId(bool aStartRequest, nsIRequest* aRequest); + + void SetChannelToDisconnectOnPageHide(uint64_t aChannelId) { + MOZ_ASSERT(mChannelToDisconnectOnPageHide == 0); + mChannelToDisconnectOnPageHide = aChannelId; + } + void MaybeDisconnectChildListenersOnPageHide(); + + /** + * Helper for addState and document.open that does just the + * history-manipulation guts. + * + * Arguments the spec defines: + * + * @param aDocument the document we're manipulating. This will get the new + * URI. + * @param aNewURI the new URI. + * @param aData The serialized state data. May be null. + * @param aTitle The new title. May be empty. + * @param aReplace whether this should replace the exising SHEntry. + * + * Arguments we need internally because deriving them from the + * others is a bit complicated: + * + * @param aCurrentURI the current URI we're working with. Might be null. + * @param aEqualURIs whether the two URIs involved are equal. + */ + nsresult UpdateURLAndHistory(mozilla::dom::Document* aDocument, + nsIURI* aNewURI, + nsIStructuredCloneContainer* aData, + const nsAString& aTitle, bool aReplace, + nsIURI* aCurrentURI, bool aEqualURIs); + + private: // data members + nsString mTitle; + nsCString mOriginalUriString; + nsTObserverArray<nsWeakPtr> mPrivacyObservers; + nsTObserverArray<nsWeakPtr> mReflowObservers; + nsTObserverArray<nsWeakPtr> mScrollObservers; + mozilla::UniquePtr<mozilla::dom::ClientSource> mInitialClientSource; + nsCOMPtr<nsINetworkInterceptController> mInterceptController; + RefPtr<nsDOMNavigationTiming> mTiming; + RefPtr<nsDSURIContentListener> mContentListener; + RefPtr<nsGlobalWindowOuter> mScriptGlobal; + nsCOMPtr<nsIPrincipal> mParentCharsetPrincipal; + // The following 3 lists contain either nsITimer or nsRefreshTimer objects. + // URIs to refresh are collected to mRefreshURIList. + nsCOMPtr<nsIMutableArray> mRefreshURIList; + // mSavedRefreshURIList is used to move the entries from mRefreshURIList to + // mOSHE. + nsCOMPtr<nsIMutableArray> mSavedRefreshURIList; + // BFCache-in-parent implementation caches the entries in + // mBFCachedRefreshURIList. + nsCOMPtr<nsIMutableArray> mBFCachedRefreshURIList; + uint64_t mContentWindowID; + nsCOMPtr<nsIContentViewer> mContentViewer; + nsCOMPtr<nsIWidget> mParentWidget; + RefPtr<mozilla::dom::ChildSHistory> mSessionHistory; + nsCOMPtr<nsIWebBrowserFind> mFind; + RefPtr<nsCommandManager> mCommandManager; + RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext; + + // Weak reference to our BrowserChild actor. + nsWeakPtr mBrowserChild; + + // Dimensions of the docshell + nsIntRect mBounds; + + /** + * Content-Type Hint of the most-recently initiated load. Used for + * session history entries. + */ + nsCString mContentTypeHint; + + // An observed docshell wrapper is created when recording markers is enabled. + mozilla::UniquePtr<mozilla::ObservedDocShell> mObserved; + + // mCurrentURI should be marked immutable on set if possible. + nsCOMPtr<nsIURI> mCurrentURI; + nsCOMPtr<nsIReferrerInfo> mReferrerInfo; + +#ifdef DEBUG + // We're counting the number of |nsDocShells| to help find leaks + static unsigned long gNumberOfDocShells; + + nsCOMPtr<nsIURI> mLastOpenedURI; +#endif + + // Reference to the SHEntry for this docshell until the page is destroyed. + // Somebody give me better name + // Only used when SHIP is disabled. + nsCOMPtr<nsISHEntry> mOSHE; + + // Reference to the SHEntry for this docshell until the page is loaded + // Somebody give me better name. + // If mLSHE is non-null, non-pushState subframe loads don't create separate + // root history entries. That is, frames loaded during the parent page + // load don't generate history entries the way frame navigation after the + // parent has loaded does. (This isn't the only purpose of mLSHE.) + // Only used when SHIP is disabled. + nsCOMPtr<nsISHEntry> mLSHE; + + // These are only set when fission.sessionHistoryInParent is set. + mozilla::UniquePtr<mozilla::dom::SessionHistoryInfo> mActiveEntry; + bool mActiveEntryIsLoadingFromSessionHistory = false; + // mLoadingEntry is set when we're about to start loading. Whenever + // setting mLoadingEntry, be sure to also set + // mNeedToReportActiveAfterLoadingBecomesActive. + mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> mLoadingEntry; + + // Holds a weak pointer to a RestorePresentationEvent object if any that + // holds a weak pointer back to us. We use this pointer to possibly revoke + // the event whenever necessary. + nsRevocableEventPtr<RestorePresentationEvent> mRestorePresentationEvent; + + // Editor data, if this document is designMode or contentEditable. + mozilla::UniquePtr<nsDocShellEditorData> mEditorData; + + // The URI we're currently loading. This is only relevant during the + // firing of a pagehide/unload. The caller of FirePageHideNotification() + // is responsible for setting it and unsetting it. It may be null if the + // pagehide/unload is happening for some reason other than just loading a + // new URI. + nsCOMPtr<nsIURI> mLoadingURI; + + // Set in LoadErrorPage from the method argument and used later + // in CreateContentViewer. We have to delay an shistory entry creation + // for which these objects are needed. + nsCOMPtr<nsIURI> mFailedURI; + nsCOMPtr<nsIChannel> mFailedChannel; + + mozilla::UniquePtr<mozilla::gfx::Matrix5x4> mColorMatrix; + + const mozilla::Encoding* mParentCharset; + + // WEAK REFERENCES BELOW HERE. + // Note these are intentionally not addrefd. Doing so will create a cycle. + // For that reasons don't use nsCOMPtr. + + nsIDocShellTreeOwner* mTreeOwner; // Weak Reference + + RefPtr<mozilla::dom::EventTarget> mChromeEventHandler; + + mozilla::ScrollbarPreference mScrollbarPref; // persistent across doc loads + + eCharsetReloadState mCharsetReloadState; + + int32_t mParentCharsetSource; + mozilla::CSSIntSize mFrameMargins; + + // This can either be a content docshell or a chrome docshell. + const int32_t mItemType; + + // Index into the nsISHEntry array, indicating the previous and current + // entry at the time that this DocShell begins to load. Consequently + // root docshell's indices can differ from child docshells'. + int32_t mPreviousEntryIndex; + int32_t mLoadedEntryIndex; + + BusyFlags mBusyFlags; + AppType mAppType; + uint32_t mLoadType; + uint32_t mFailedLoadType; + + // A depth count of how many times NotifyRunToCompletionStart + // has been called without a matching NotifyRunToCompletionStop. + uint32_t mJSRunToCompletionDepth; + + // Whether or not handling of the <meta name="viewport"> tag is overridden. + // Possible values are defined as constants in nsIDocShell.idl. + MetaViewportOverride mMetaViewportOverride; + + // See WindowGlobalParent::mSingleChannelId. + mozilla::Maybe<uint64_t> mSingleChannelId; + uint32_t mRequestForBlockingFromBFCacheCount = 0; + + uint64_t mChannelToDisconnectOnPageHide; + + uint32_t mPendingReloadCount = 0; + + // The following two fields cannot be declared as bit fields + // because of uses with AutoRestore. + bool mCreatingDocument; // (should be) debugging only +#ifdef DEBUG + bool mInEnsureScriptEnv; + uint64_t mDocShellID = 0; +#endif + + bool mInitialized : 1; + bool mAllowSubframes : 1; + bool mAllowMetaRedirects : 1; + bool mAllowImages : 1; + bool mAllowMedia : 1; + bool mAllowDNSPrefetch : 1; + bool mAllowWindowControl : 1; + bool mCSSErrorReportingEnabled : 1; + bool mAllowAuth : 1; + bool mAllowKeywordFixup : 1; + bool mDisableMetaRefreshWhenInactive : 1; + bool mIsAppTab : 1; + bool mWindowDraggingAllowed : 1; + bool mInFrameSwap : 1; + + // This boolean is set to true right before we fire pagehide and generally + // unset when we embed a new content viewer. While it's true no navigation + // is allowed in this docshell. + bool mFiredUnloadEvent : 1; + + // this flag is for bug #21358. a docshell may load many urls + // which don't result in new documents being created (i.e. a new + // content viewer) we want to make sure we don't call a on load + // event more than once for a given content viewer. + bool mEODForCurrentDocument : 1; + bool mURIResultedInDocument : 1; + + bool mIsBeingDestroyed : 1; + + bool mIsExecutingOnLoadHandler : 1; + + // Indicates to CreateContentViewer() that it is safe to cache the old + // presentation of the page, and to SetupNewViewer() that the old viewer + // should be passed a SHEntry to save itself into. + // Only used with SHIP disabled. + bool mSavingOldViewer : 1; + + bool mInvisible : 1; + bool mHasLoadedNonBlankURI : 1; + + // This flag means that mTiming has been initialized but nulled out. + // We will check the innerWin's timing before creating a new one + // in MaybeInitTiming() + bool mBlankTiming : 1; + + // This flag indicates when the title is valid for the current URI. + bool mTitleValidForCurrentURI : 1; + + // If mWillChangeProcess is set to true, then when the docshell is destroyed, + // we prepare the browsing context to change process. + bool mWillChangeProcess : 1; + + // This flag indicates whether or not the DocShell is currently executing an + // nsIWebNavigation navigation method. + bool mIsNavigating : 1; + + // Whether we have a pending encoding autodetection request from the + // menu for all encodings. + bool mForcedAutodetection : 1; + + /* + * Set to true if we're checking session history (in the parent process) for + * a possible history load. Used only with iframes. + */ + bool mCheckingSessionHistory : 1; + + // Whether mBrowsingContext->SetActiveSessionHistoryEntry() needs to be called + // when the loading entry becomes the active entry. This is used for the + // initial about:blank-replacing about:blank in order to make the history + // length WPTs pass. + bool mNeedToReportActiveAfterLoadingBecomesActive : 1; +}; + +inline nsISupports* ToSupports(nsDocShell* aDocShell) { + return static_cast<nsIDocumentLoader*>(aDocShell); +} + +#endif /* nsDocShell_h__ */ diff --git a/docshell/base/nsDocShellEditorData.cpp b/docshell/base/nsDocShellEditorData.cpp new file mode 100644 index 0000000000..6fe132a977 --- /dev/null +++ b/docshell/base/nsDocShellEditorData.cpp @@ -0,0 +1,139 @@ +/* -*- 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/. */ + +#include "nsDocShellEditorData.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/HTMLEditor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsPIDOMWindow.h" +#include "nsEditingSession.h" +#include "nsIDocShell.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsDocShellEditorData::nsDocShellEditorData(nsIDocShell* aOwningDocShell) + : mDocShell(aOwningDocShell), + mDetachedEditingState(Document::EditingState::eOff), + mMakeEditable(false), + mIsDetached(false), + mDetachedMakeEditable(false) { + NS_ASSERTION(mDocShell, "Where is my docShell?"); +} + +nsDocShellEditorData::~nsDocShellEditorData() { TearDownEditor(); } + +void nsDocShellEditorData::TearDownEditor() { + if (mHTMLEditor) { + RefPtr<HTMLEditor> htmlEditor = std::move(mHTMLEditor); + htmlEditor->PreDestroy(); + } + mEditingSession = nullptr; + mIsDetached = false; +} + +nsresult nsDocShellEditorData::MakeEditable(bool aInWaitForUriLoad) { + if (mMakeEditable) { + return NS_OK; + } + + // if we are already editable, and are getting turned off, + // nuke the editor. + if (mHTMLEditor) { + NS_WARNING("Destroying existing editor on frame"); + + RefPtr<HTMLEditor> htmlEditor = std::move(mHTMLEditor); + htmlEditor->PreDestroy(); + } + + if (aInWaitForUriLoad) { + mMakeEditable = true; + } + return NS_OK; +} + +bool nsDocShellEditorData::GetEditable() { + return mMakeEditable || (mHTMLEditor != nullptr); +} + +nsEditingSession* nsDocShellEditorData::GetEditingSession() { + EnsureEditingSession(); + + return mEditingSession.get(); +} + +nsresult nsDocShellEditorData::SetHTMLEditor(HTMLEditor* aHTMLEditor) { + // destroy any editor that we have. Checks for equality are + // necessary to ensure that assigment into the nsCOMPtr does + // not temporarily reduce the refCount of the editor to zero + if (mHTMLEditor == aHTMLEditor) { + return NS_OK; + } + + if (mHTMLEditor) { + RefPtr<HTMLEditor> htmlEditor = std::move(mHTMLEditor); + htmlEditor->PreDestroy(); + MOZ_ASSERT(!mHTMLEditor, + "Nested call of nsDocShellEditorData::SetHTMLEditor() detected"); + } + + mHTMLEditor = aHTMLEditor; // owning addref + if (!mHTMLEditor) { + mMakeEditable = false; + } + + return NS_OK; +} + +// This creates the editing session on the content docShell that owns 'this'. +void nsDocShellEditorData::EnsureEditingSession() { + NS_ASSERTION(mDocShell, "Should have docShell here"); + NS_ASSERTION(!mIsDetached, "This will stomp editing session!"); + + if (!mEditingSession) { + mEditingSession = new nsEditingSession(); + } +} + +nsresult nsDocShellEditorData::DetachFromWindow() { + NS_ASSERTION(mEditingSession, + "Can't detach when we don't have a session to detach!"); + + nsCOMPtr<nsPIDOMWindowOuter> domWindow = + mDocShell ? mDocShell->GetWindow() : nullptr; + nsresult rv = mEditingSession->DetachFromWindow(domWindow); + NS_ENSURE_SUCCESS(rv, rv); + + mIsDetached = true; + mDetachedMakeEditable = mMakeEditable; + mMakeEditable = false; + + nsCOMPtr<dom::Document> doc = domWindow->GetDoc(); + mDetachedEditingState = doc->GetEditingState(); + + mDocShell = nullptr; + + return NS_OK; +} + +nsresult nsDocShellEditorData::ReattachToWindow(nsIDocShell* aDocShell) { + mDocShell = aDocShell; + + nsCOMPtr<nsPIDOMWindowOuter> domWindow = + mDocShell ? mDocShell->GetWindow() : nullptr; + nsresult rv = mEditingSession->ReattachToWindow(domWindow); + NS_ENSURE_SUCCESS(rv, rv); + + mIsDetached = false; + mMakeEditable = mDetachedMakeEditable; + + RefPtr<dom::Document> doc = domWindow->GetDoc(); + doc->SetEditingState(mDetachedEditingState); + + return NS_OK; +} diff --git a/docshell/base/nsDocShellEditorData.h b/docshell/base/nsDocShellEditorData.h new file mode 100644 index 0000000000..27f840675b --- /dev/null +++ b/docshell/base/nsDocShellEditorData.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ +#ifndef nsDocShellEditorData_h__ +#define nsDocShellEditorData_h__ + +#ifndef nsCOMPtr_h___ +# include "nsCOMPtr.h" +#endif + +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Document.h" + +class nsIDocShell; +class nsEditingSession; + +namespace mozilla { +class HTMLEditor; +} + +class nsDocShellEditorData { + public: + explicit nsDocShellEditorData(nsIDocShell* aOwningDocShell); + ~nsDocShellEditorData(); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult MakeEditable(bool aWaitForUriLoad); + bool GetEditable(); + nsEditingSession* GetEditingSession(); + mozilla::HTMLEditor* GetHTMLEditor() const { return mHTMLEditor; } + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult + SetHTMLEditor(mozilla::HTMLEditor* aHTMLEditor); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void TearDownEditor(); + nsresult DetachFromWindow(); + nsresult ReattachToWindow(nsIDocShell* aDocShell); + bool WaitingForLoad() const { return mMakeEditable; } + + protected: + void EnsureEditingSession(); + + // The doc shell that owns us. Weak ref, since it always outlives us. + nsIDocShell* mDocShell; + + // Only present for the content root docShell. Session is owned here. + RefPtr<nsEditingSession> mEditingSession; + + // If this frame is editable, store HTML editor here. It's owned here. + RefPtr<mozilla::HTMLEditor> mHTMLEditor; + + // Backup for the corresponding HTMLDocument's editing state while + // the editor is detached. + mozilla::dom::Document::EditingState mDetachedEditingState; + + // Indicates whether to make an editor after a url load. + bool mMakeEditable; + + // Denotes if the editor is detached from its window. The editor is detached + // while it's stored in the session history bfcache. + bool mIsDetached; + + // Backup for mMakeEditable while the editor is detached. + bool mDetachedMakeEditable; +}; + +#endif // nsDocShellEditorData_h__ diff --git a/docshell/base/nsDocShellEnumerator.cpp b/docshell/base/nsDocShellEnumerator.cpp new file mode 100644 index 0000000000..5ad0ad35e6 --- /dev/null +++ b/docshell/base/nsDocShellEnumerator.cpp @@ -0,0 +1,85 @@ +/* -*- 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/. */ + +#include "nsDocShellEnumerator.h" + +#include "nsDocShell.h" + +using namespace mozilla; + +nsDocShellEnumerator::nsDocShellEnumerator( + nsDocShellEnumerator::EnumerationDirection aDirection, + int32_t aDocShellType, nsDocShell& aRootItem) + : mRootItem(&aRootItem), + mDocShellType(aDocShellType), + mDirection(aDirection) {} + +nsresult nsDocShellEnumerator::BuildDocShellArray( + nsTArray<RefPtr<nsIDocShell>>& aItemArray) { + MOZ_ASSERT(mRootItem); + + aItemArray.Clear(); + + if (mDirection == EnumerationDirection::Forwards) { + return BuildArrayRecursiveForwards(mRootItem, aItemArray); + } + MOZ_ASSERT(mDirection == EnumerationDirection::Backwards); + return BuildArrayRecursiveBackwards(mRootItem, aItemArray); +} + +nsresult nsDocShellEnumerator::BuildArrayRecursiveForwards( + nsDocShell* aItem, nsTArray<RefPtr<nsIDocShell>>& aItemArray) { + nsresult rv; + + // add this item to the array + if (mDocShellType == nsIDocShellTreeItem::typeAll || + aItem->ItemType() == mDocShellType) { + if (!aItemArray.AppendElement(aItem, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + int32_t numChildren = aItem->ChildCount(); + + for (int32_t i = 0; i < numChildren; ++i) { + RefPtr<nsDocShell> curChild = aItem->GetInProcessChildAt(i); + MOZ_ASSERT(curChild); + + rv = BuildArrayRecursiveForwards(curChild, aItemArray); + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +nsresult nsDocShellEnumerator::BuildArrayRecursiveBackwards( + nsDocShell* aItem, nsTArray<RefPtr<nsIDocShell>>& aItemArray) { + nsresult rv; + + uint32_t numChildren = aItem->ChildCount(); + + for (int32_t i = numChildren - 1; i >= 0; --i) { + RefPtr<nsDocShell> curChild = aItem->GetInProcessChildAt(i); + MOZ_ASSERT(curChild); + + rv = BuildArrayRecursiveBackwards(curChild, aItemArray); + if (NS_FAILED(rv)) { + return rv; + } + } + + // add this item to the array + if (mDocShellType == nsIDocShellTreeItem::typeAll || + aItem->ItemType() == mDocShellType) { + if (!aItemArray.AppendElement(aItem, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return NS_OK; +} diff --git a/docshell/base/nsDocShellEnumerator.h b/docshell/base/nsDocShellEnumerator.h new file mode 100644 index 0000000000..668ddee7e9 --- /dev/null +++ b/docshell/base/nsDocShellEnumerator.h @@ -0,0 +1,39 @@ +/* -*- 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/. */ + +#ifndef nsDocShellEnumerator_h___ +#define nsDocShellEnumerator_h___ + +#include "nsTArray.h" + +class nsDocShell; +class nsIDocShell; + +class MOZ_STACK_CLASS nsDocShellEnumerator { + public: + enum class EnumerationDirection : uint8_t { Forwards, Backwards }; + + nsDocShellEnumerator(EnumerationDirection aDirection, int32_t aDocShellType, + nsDocShell& aRootItem); + + public: + nsresult BuildDocShellArray(nsTArray<RefPtr<nsIDocShell>>& aItemArray); + + private: + nsresult BuildArrayRecursiveForwards( + nsDocShell* aItem, nsTArray<RefPtr<nsIDocShell>>& aItemArray); + nsresult BuildArrayRecursiveBackwards( + nsDocShell* aItem, nsTArray<RefPtr<nsIDocShell>>& aItemArray); + + private: + const RefPtr<nsDocShell> mRootItem; + + const int32_t mDocShellType; // only want shells of this type + + const EnumerationDirection mDirection; +}; + +#endif // nsDocShellEnumerator_h___ diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp new file mode 100644 index 0000000000..f12e146fbd --- /dev/null +++ b/docshell/base/nsDocShellLoadState.cpp @@ -0,0 +1,1266 @@ +/* -*- 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/. */ + +#include "nsDocShellLoadState.h" +#include "nsIDocShell.h" +#include "nsDocShell.h" +#include "nsISHEntry.h" +#include "nsIURIFixup.h" +#include "nsIWebNavigation.h" +#include "nsIChannel.h" +#include "nsIURLQueryStringStripper.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "ReferrerInfo.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Components.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/LoadURIOptionsBinding.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/Telemetry.h" + +#include "mozilla/OriginAttributes.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/StaticPtr.h" + +#include "mozilla/dom/PContent.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// Global reference to the URI fixup service. +static mozilla::StaticRefPtr<nsIURIFixup> sURIFixup; + +nsDocShellLoadState::nsDocShellLoadState(nsIURI* aURI) + : nsDocShellLoadState(aURI, nsContentUtils::GenerateLoadIdentifier()) {} + +nsDocShellLoadState::nsDocShellLoadState( + const DocShellLoadStateInit& aLoadState, mozilla::ipc::IProtocol* aActor, + bool* aReadSuccess) + : mNotifiedBeforeUnloadListeners(false), + mLoadIdentifier(aLoadState.LoadIdentifier()) { + // If we return early, we failed to read in the data. + *aReadSuccess = false; + if (!aLoadState.URI()) { + MOZ_ASSERT_UNREACHABLE("Cannot create a LoadState with a null URI!"); + return; + } + + mResultPrincipalURI = aLoadState.ResultPrincipalURI(); + mResultPrincipalURIIsSome = aLoadState.ResultPrincipalURIIsSome(); + mKeepResultPrincipalURIIfSet = aLoadState.KeepResultPrincipalURIIfSet(); + mLoadReplace = aLoadState.LoadReplace(); + mInheritPrincipal = aLoadState.InheritPrincipal(); + mPrincipalIsExplicit = aLoadState.PrincipalIsExplicit(); + mForceAllowDataURI = aLoadState.ForceAllowDataURI(); + mIsExemptFromHTTPSOnlyMode = aLoadState.IsExemptFromHTTPSOnlyMode(); + mOriginalFrameSrc = aLoadState.OriginalFrameSrc(); + mIsFormSubmission = aLoadState.IsFormSubmission(); + mLoadType = aLoadState.LoadType(); + mTarget = aLoadState.Target(); + mTargetBrowsingContext = aLoadState.TargetBrowsingContext(); + mLoadFlags = aLoadState.LoadFlags(); + mInternalLoadFlags = aLoadState.InternalLoadFlags(); + mFirstParty = aLoadState.FirstParty(); + mHasValidUserGestureActivation = aLoadState.HasValidUserGestureActivation(); + mAllowFocusMove = aLoadState.AllowFocusMove(); + mTypeHint = aLoadState.TypeHint(); + mFileName = aLoadState.FileName(); + mIsFromProcessingFrameAttributes = + aLoadState.IsFromProcessingFrameAttributes(); + mReferrerInfo = aLoadState.ReferrerInfo(); + mURI = aLoadState.URI(); + mOriginalURI = aLoadState.OriginalURI(); + mSourceBrowsingContext = aLoadState.SourceBrowsingContext(); + mBaseURI = aLoadState.BaseURI(); + mTriggeringPrincipal = aLoadState.TriggeringPrincipal(); + mPrincipalToInherit = aLoadState.PrincipalToInherit(); + mPartitionedPrincipalToInherit = aLoadState.PartitionedPrincipalToInherit(); + mTriggeringSandboxFlags = aLoadState.TriggeringSandboxFlags(); + mTriggeringRemoteType = aLoadState.TriggeringRemoteType(); + mCsp = aLoadState.Csp(); + mOriginalURIString = aLoadState.OriginalURIString(); + mCancelContentJSEpoch = aLoadState.CancelContentJSEpoch(); + mPostDataStream = aLoadState.PostDataStream(); + mHeadersStream = aLoadState.HeadersStream(); + mSrcdocData = aLoadState.SrcdocData(); + mChannelInitialized = aLoadState.ChannelInitialized(); + mIsMetaRefresh = aLoadState.IsMetaRefresh(); + if (aLoadState.loadingSessionHistoryInfo().isSome()) { + mLoadingSessionHistoryInfo = MakeUnique<LoadingSessionHistoryInfo>( + aLoadState.loadingSessionHistoryInfo().ref()); + } + mUnstrippedURI = aLoadState.UnstrippedURI(); + mRemoteTypeOverride = aLoadState.RemoteTypeOverride(); + + // We know this was created remotely, as we just received it over IPC. + mWasCreatedRemotely = true; + + // If we're in the parent process, potentially validate against a LoadState + // which we sent to the source content process. + if (XRE_IsParentProcess()) { + mozilla::ipc::IToplevelProtocol* top = aActor->ToplevelProtocol(); + if (!top || + top->GetProtocolId() != mozilla::ipc::ProtocolId::PContentMsgStart || + top->GetSide() != mozilla::ipc::ParentSide) { + aActor->FatalError("nsDocShellLoadState must be received over PContent"); + return; + } + ContentParent* cp = static_cast<ContentParent*>(top); + + // If this load was sent down to the content process as a navigation + // request, ensure it still matches the one we sent down. + if (RefPtr<nsDocShellLoadState> originalState = + cp->TakePendingLoadStateForId(mLoadIdentifier)) { + if (const char* mismatch = ValidateWithOriginalState(originalState)) { + aActor->FatalError( + nsPrintfCString( + "nsDocShellLoadState %s changed while in content process", + mismatch) + .get()); + return; + } + } else if (mTriggeringRemoteType != cp->GetRemoteType()) { + // If we don't have a previous load to compare to, the content process + // must be the triggering process. + aActor->FatalError( + "nsDocShellLoadState with invalid triggering remote type"); + return; + } + } + + // We successfully read in the data - return a success value. + *aReadSuccess = true; +} + +nsDocShellLoadState::nsDocShellLoadState(const nsDocShellLoadState& aOther) + : mReferrerInfo(aOther.mReferrerInfo), + mURI(aOther.mURI), + mOriginalURI(aOther.mOriginalURI), + mResultPrincipalURI(aOther.mResultPrincipalURI), + mResultPrincipalURIIsSome(aOther.mResultPrincipalURIIsSome), + mTriggeringPrincipal(aOther.mTriggeringPrincipal), + mTriggeringSandboxFlags(aOther.mTriggeringSandboxFlags), + mCsp(aOther.mCsp), + mKeepResultPrincipalURIIfSet(aOther.mKeepResultPrincipalURIIfSet), + mLoadReplace(aOther.mLoadReplace), + mInheritPrincipal(aOther.mInheritPrincipal), + mPrincipalIsExplicit(aOther.mPrincipalIsExplicit), + mNotifiedBeforeUnloadListeners(aOther.mNotifiedBeforeUnloadListeners), + mPrincipalToInherit(aOther.mPrincipalToInherit), + mPartitionedPrincipalToInherit(aOther.mPartitionedPrincipalToInherit), + mForceAllowDataURI(aOther.mForceAllowDataURI), + mIsExemptFromHTTPSOnlyMode(aOther.mIsExemptFromHTTPSOnlyMode), + mOriginalFrameSrc(aOther.mOriginalFrameSrc), + mIsFormSubmission(aOther.mIsFormSubmission), + mLoadType(aOther.mLoadType), + mSHEntry(aOther.mSHEntry), + mTarget(aOther.mTarget), + mTargetBrowsingContext(aOther.mTargetBrowsingContext), + mPostDataStream(aOther.mPostDataStream), + mHeadersStream(aOther.mHeadersStream), + mSrcdocData(aOther.mSrcdocData), + mSourceBrowsingContext(aOther.mSourceBrowsingContext), + mBaseURI(aOther.mBaseURI), + mLoadFlags(aOther.mLoadFlags), + mInternalLoadFlags(aOther.mInternalLoadFlags), + mFirstParty(aOther.mFirstParty), + mHasValidUserGestureActivation(aOther.mHasValidUserGestureActivation), + mAllowFocusMove(aOther.mAllowFocusMove), + mTypeHint(aOther.mTypeHint), + mFileName(aOther.mFileName), + mIsFromProcessingFrameAttributes(aOther.mIsFromProcessingFrameAttributes), + mPendingRedirectedChannel(aOther.mPendingRedirectedChannel), + mOriginalURIString(aOther.mOriginalURIString), + mCancelContentJSEpoch(aOther.mCancelContentJSEpoch), + mLoadIdentifier(aOther.mLoadIdentifier), + mChannelInitialized(aOther.mChannelInitialized), + mIsMetaRefresh(aOther.mIsMetaRefresh), + mWasCreatedRemotely(aOther.mWasCreatedRemotely), + mUnstrippedURI(aOther.mUnstrippedURI), + mRemoteTypeOverride(aOther.mRemoteTypeOverride), + mTriggeringRemoteType(aOther.mTriggeringRemoteType) { + MOZ_DIAGNOSTIC_ASSERT( + XRE_IsParentProcess(), + "Cloning a nsDocShellLoadState with the same load identifier is only " + "allowed in the parent process, as it could break triggering remote type " + "tracking in content."); + if (aOther.mLoadingSessionHistoryInfo) { + mLoadingSessionHistoryInfo = MakeUnique<LoadingSessionHistoryInfo>( + *aOther.mLoadingSessionHistoryInfo); + } +} + +nsDocShellLoadState::nsDocShellLoadState(nsIURI* aURI, uint64_t aLoadIdentifier) + : mURI(aURI), + mResultPrincipalURIIsSome(false), + mTriggeringSandboxFlags(0), + mKeepResultPrincipalURIIfSet(false), + mLoadReplace(false), + mInheritPrincipal(false), + mPrincipalIsExplicit(false), + mNotifiedBeforeUnloadListeners(false), + mForceAllowDataURI(false), + mIsExemptFromHTTPSOnlyMode(false), + mOriginalFrameSrc(false), + mIsFormSubmission(false), + mLoadType(LOAD_NORMAL), + mTarget(), + mSrcdocData(VoidString()), + mLoadFlags(0), + mInternalLoadFlags(0), + mFirstParty(false), + mHasValidUserGestureActivation(false), + mAllowFocusMove(false), + mTypeHint(VoidCString()), + mFileName(VoidString()), + mIsFromProcessingFrameAttributes(false), + mLoadIdentifier(aLoadIdentifier), + mChannelInitialized(false), + mIsMetaRefresh(false), + mWasCreatedRemotely(false), + mTriggeringRemoteType(XRE_IsContentProcess() + ? ContentChild::GetSingleton()->GetRemoteType() + : NOT_REMOTE_TYPE) { + MOZ_ASSERT(aURI, "Cannot create a LoadState with a null URI!"); +} + +nsDocShellLoadState::~nsDocShellLoadState() { + if (mWasCreatedRemotely && XRE_IsContentProcess()) { + ContentChild::GetSingleton()->SendCleanupPendingLoadState(mLoadIdentifier); + } +} + +nsresult nsDocShellLoadState::CreateFromPendingChannel( + nsIChannel* aPendingChannel, uint64_t aLoadIdentifier, + uint64_t aRegistrarId, nsDocShellLoadState** aResult) { + // Create the nsDocShellLoadState object with default state pulled from the + // passed-in channel. + nsCOMPtr<nsIURI> uri; + nsresult rv = aPendingChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<nsDocShellLoadState> loadState = + new nsDocShellLoadState(uri, aLoadIdentifier); + loadState->mPendingRedirectedChannel = aPendingChannel; + loadState->mChannelRegistrarId = aRegistrarId; + + // Pull relevant state from the channel, and store it on the + // nsDocShellLoadState. + nsCOMPtr<nsIURI> originalUri; + rv = aPendingChannel->GetOriginalURI(getter_AddRefs(originalUri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + loadState->SetOriginalURI(originalUri); + + nsCOMPtr<nsILoadInfo> loadInfo = aPendingChannel->LoadInfo(); + loadState->SetTriggeringPrincipal(loadInfo->TriggeringPrincipal()); + + // Return the newly created loadState. + loadState.forget(aResult); + return NS_OK; +} + +static uint32_t WebNavigationFlagsToFixupFlags(nsIURI* aURI, + const nsACString& aURIString, + uint32_t aNavigationFlags) { + if (aURI) { + aNavigationFlags &= ~nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + } + uint32_t fixupFlags = nsIURIFixup::FIXUP_FLAG_NONE; + if (aNavigationFlags & nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) { + fixupFlags |= nsIURIFixup::FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + } + if (aNavigationFlags & nsIWebNavigation::LOAD_FLAGS_FIXUP_SCHEME_TYPOS) { + fixupFlags |= nsIURIFixup::FIXUP_FLAG_FIX_SCHEME_TYPOS; + } + return fixupFlags; +}; + +nsresult nsDocShellLoadState::CreateFromLoadURIOptions( + BrowsingContext* aBrowsingContext, const nsAString& aURI, + const LoadURIOptions& aLoadURIOptions, nsDocShellLoadState** aResult) { + uint32_t loadFlags = aLoadURIOptions.mLoadFlags; + + NS_ASSERTION( + (loadFlags & nsDocShell::INTERNAL_LOAD_FLAGS_LOADURI_SETUP_FLAGS) == 0, + "Unexpected flags"); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_OK; + + NS_ConvertUTF16toUTF8 uriString(aURI); + // Cleanup the empty spaces that might be on each end. + uriString.Trim(" "); + // Eliminate embedded newlines, which single-line text fields now allow: + uriString.StripCRLF(); + NS_ENSURE_TRUE(!uriString.IsEmpty(), NS_ERROR_FAILURE); + + // Just create a URI and see what happens... + rv = NS_NewURI(getter_AddRefs(uri), uriString); + bool fixup = true; + if (NS_SUCCEEDED(rv) && uri && + (uri->SchemeIs("about") || uri->SchemeIs("chrome"))) { + // Avoid third party fixup as a performance optimization. + loadFlags &= ~nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + fixup = false; + } else if (!sURIFixup && !XRE_IsContentProcess()) { + nsCOMPtr<nsIURIFixup> uriFixup = components::URIFixup::Service(); + if (uriFixup) { + sURIFixup = uriFixup; + ClearOnShutdown(&sURIFixup); + } else { + fixup = false; + } + } + + nsAutoString searchProvider, keyword; + RefPtr<nsIInputStream> fixupStream; + if (fixup) { + uint32_t fixupFlags = + WebNavigationFlagsToFixupFlags(uri, uriString, loadFlags); + + // If we don't allow keyword lookups for this URL string, make sure to + // update loadFlags to indicate this as well. + if (!(fixupFlags & nsIURIFixup::FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP)) { + loadFlags &= ~nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + } + // Ensure URIFixup will use the right search engine in Private Browsing. + if (aBrowsingContext->UsePrivateBrowsing()) { + fixupFlags |= nsIURIFixup::FIXUP_FLAG_PRIVATE_CONTEXT; + } + + if (!XRE_IsContentProcess()) { + nsCOMPtr<nsIURIFixupInfo> fixupInfo; + sURIFixup->GetFixupURIInfo(uriString, fixupFlags, + getter_AddRefs(fixupInfo)); + if (fixupInfo) { + // We could fix the uri, clear NS_ERROR_MALFORMED_URI. + rv = NS_OK; + fixupInfo->GetPreferredURI(getter_AddRefs(uri)); + fixupInfo->SetConsumer(aBrowsingContext); + fixupInfo->GetKeywordProviderName(searchProvider); + fixupInfo->GetKeywordAsSent(keyword); + // GetFixupURIInfo only returns a post data stream if it succeeded + // and changed the URI, in which case we should override the + // passed-in post data by passing this as an override arg to + // our internal method. + fixupInfo->GetPostData(getter_AddRefs(fixupStream)); + + if (fixupInfo && + loadFlags & nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) { + nsCOMPtr<nsIObserverService> serv = services::GetObserverService(); + if (serv) { + serv->NotifyObservers(fixupInfo, "keyword-uri-fixup", + PromiseFlatString(aURI).get()); + } + } + nsDocShell::MaybeNotifyKeywordSearchLoading(searchProvider, keyword); + } + } + } + + if (rv == NS_ERROR_MALFORMED_URI) { + MOZ_ASSERT(!uri); + return rv; + } + + if (NS_FAILED(rv) || !uri) { + return NS_ERROR_FAILURE; + } + + RefPtr<nsDocShellLoadState> loadState; + rv = CreateFromLoadURIOptions( + aBrowsingContext, uri, aLoadURIOptions, loadFlags, + fixupStream ? fixupStream : aLoadURIOptions.mPostData, + getter_AddRefs(loadState)); + NS_ENSURE_SUCCESS(rv, rv); + loadState->SetOriginalURIString(uriString); + loadState.forget(aResult); + return NS_OK; +} + +nsresult nsDocShellLoadState::CreateFromLoadURIOptions( + BrowsingContext* aBrowsingContext, nsIURI* aURI, + const LoadURIOptions& aLoadURIOptions, nsDocShellLoadState** aResult) { + return CreateFromLoadURIOptions(aBrowsingContext, aURI, aLoadURIOptions, + aLoadURIOptions.mLoadFlags, + aLoadURIOptions.mPostData, aResult); +} + +nsresult nsDocShellLoadState::CreateFromLoadURIOptions( + BrowsingContext* aBrowsingContext, nsIURI* aURI, + const LoadURIOptions& aLoadURIOptions, uint32_t aLoadFlagsOverride, + nsIInputStream* aPostDataOverride, nsDocShellLoadState** aResult) { + nsresult rv = NS_OK; + uint32_t loadFlags = aLoadFlagsOverride; + RefPtr<nsIInputStream> postData = aPostDataOverride; + uint64_t available; + if (postData) { + rv = postData->Available(&available); + NS_ENSURE_SUCCESS(rv, rv); + if (available == 0) { + return NS_ERROR_INVALID_ARG; + } + } + + if (aLoadURIOptions.mHeaders) { + rv = aLoadURIOptions.mHeaders->Available(&available); + NS_ENSURE_SUCCESS(rv, rv); + if (available == 0) { + return NS_ERROR_INVALID_ARG; + } + } + + bool forceAllowDataURI = + loadFlags & nsIWebNavigation::LOAD_FLAGS_FORCE_ALLOW_DATA_URI; + + // Don't pass certain flags that aren't needed and end up confusing + // ConvertLoadTypeToDocShellInfoLoadType. We do need to ensure that they are + // passed to LoadURI though, since it uses them. + uint32_t extraFlags = (loadFlags & EXTRA_LOAD_FLAGS); + loadFlags &= ~EXTRA_LOAD_FLAGS; + + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(aURI); + loadState->SetReferrerInfo(aLoadURIOptions.mReferrerInfo); + + loadState->SetLoadType(MAKE_LOAD_TYPE(LOAD_NORMAL, loadFlags)); + + loadState->SetLoadFlags(extraFlags); + loadState->SetFirstParty(true); + loadState->SetHasValidUserGestureActivation( + aLoadURIOptions.mHasValidUserGestureActivation); + loadState->SetTriggeringSandboxFlags(aLoadURIOptions.mTriggeringSandboxFlags); + loadState->SetPostDataStream(postData); + loadState->SetHeadersStream(aLoadURIOptions.mHeaders); + loadState->SetBaseURI(aLoadURIOptions.mBaseURI); + loadState->SetTriggeringPrincipal(aLoadURIOptions.mTriggeringPrincipal); + loadState->SetCsp(aLoadURIOptions.mCsp); + loadState->SetForceAllowDataURI(forceAllowDataURI); + if (aLoadURIOptions.mCancelContentJSEpoch) { + loadState->SetCancelContentJSEpoch(aLoadURIOptions.mCancelContentJSEpoch); + } + + if (aLoadURIOptions.mTriggeringRemoteType.WasPassed()) { + if (XRE_IsParentProcess()) { + loadState->SetTriggeringRemoteType( + aLoadURIOptions.mTriggeringRemoteType.Value()); + } else if (ContentChild::GetSingleton()->GetRemoteType() != + aLoadURIOptions.mTriggeringRemoteType.Value()) { + NS_WARNING("Invalid TriggeringRemoteType from LoadURIOptions in content"); + return NS_ERROR_INVALID_ARG; + } + } + + if (aLoadURIOptions.mRemoteTypeOverride.WasPassed()) { + loadState->SetRemoteTypeOverride( + aLoadURIOptions.mRemoteTypeOverride.Value()); + } + + loadState.forget(aResult); + return NS_OK; +} + +nsIReferrerInfo* nsDocShellLoadState::GetReferrerInfo() const { + return mReferrerInfo; +} + +void nsDocShellLoadState::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + mReferrerInfo = aReferrerInfo; +} + +nsIURI* nsDocShellLoadState::URI() const { return mURI; } + +void nsDocShellLoadState::SetURI(nsIURI* aURI) { mURI = aURI; } + +nsIURI* nsDocShellLoadState::OriginalURI() const { return mOriginalURI; } + +void nsDocShellLoadState::SetOriginalURI(nsIURI* aOriginalURI) { + mOriginalURI = aOriginalURI; +} + +nsIURI* nsDocShellLoadState::ResultPrincipalURI() const { + return mResultPrincipalURI; +} + +void nsDocShellLoadState::SetResultPrincipalURI(nsIURI* aResultPrincipalURI) { + mResultPrincipalURI = aResultPrincipalURI; +} + +bool nsDocShellLoadState::ResultPrincipalURIIsSome() const { + return mResultPrincipalURIIsSome; +} + +void nsDocShellLoadState::SetResultPrincipalURIIsSome(bool aIsSome) { + mResultPrincipalURIIsSome = aIsSome; +} + +bool nsDocShellLoadState::KeepResultPrincipalURIIfSet() const { + return mKeepResultPrincipalURIIfSet; +} + +void nsDocShellLoadState::SetKeepResultPrincipalURIIfSet(bool aKeep) { + mKeepResultPrincipalURIIfSet = aKeep; +} + +bool nsDocShellLoadState::LoadReplace() const { return mLoadReplace; } + +void nsDocShellLoadState::SetLoadReplace(bool aLoadReplace) { + mLoadReplace = aLoadReplace; +} + +nsIPrincipal* nsDocShellLoadState::TriggeringPrincipal() const { + return mTriggeringPrincipal; +} + +void nsDocShellLoadState::SetTriggeringPrincipal( + nsIPrincipal* aTriggeringPrincipal) { + mTriggeringPrincipal = aTriggeringPrincipal; +} + +nsIPrincipal* nsDocShellLoadState::PrincipalToInherit() const { + return mPrincipalToInherit; +} + +void nsDocShellLoadState::SetPrincipalToInherit( + nsIPrincipal* aPrincipalToInherit) { + mPrincipalToInherit = aPrincipalToInherit; +} + +nsIPrincipal* nsDocShellLoadState::PartitionedPrincipalToInherit() const { + return mPartitionedPrincipalToInherit; +} + +void nsDocShellLoadState::SetPartitionedPrincipalToInherit( + nsIPrincipal* aPartitionedPrincipalToInherit) { + mPartitionedPrincipalToInherit = aPartitionedPrincipalToInherit; +} + +void nsDocShellLoadState::SetCsp(nsIContentSecurityPolicy* aCsp) { + mCsp = aCsp; +} + +nsIContentSecurityPolicy* nsDocShellLoadState::Csp() const { return mCsp; } + +void nsDocShellLoadState::SetTriggeringSandboxFlags(uint32_t flags) { + mTriggeringSandboxFlags = flags; +} + +uint32_t nsDocShellLoadState::TriggeringSandboxFlags() const { + return mTriggeringSandboxFlags; +} + +bool nsDocShellLoadState::InheritPrincipal() const { return mInheritPrincipal; } + +void nsDocShellLoadState::SetInheritPrincipal(bool aInheritPrincipal) { + mInheritPrincipal = aInheritPrincipal; +} + +bool nsDocShellLoadState::PrincipalIsExplicit() const { + return mPrincipalIsExplicit; +} + +void nsDocShellLoadState::SetPrincipalIsExplicit(bool aPrincipalIsExplicit) { + mPrincipalIsExplicit = aPrincipalIsExplicit; +} + +bool nsDocShellLoadState::NotifiedBeforeUnloadListeners() const { + return mNotifiedBeforeUnloadListeners; +} + +void nsDocShellLoadState::SetNotifiedBeforeUnloadListeners( + bool aNotifiedBeforeUnloadListeners) { + mNotifiedBeforeUnloadListeners = aNotifiedBeforeUnloadListeners; +} + +bool nsDocShellLoadState::ForceAllowDataURI() const { + return mForceAllowDataURI; +} + +void nsDocShellLoadState::SetForceAllowDataURI(bool aForceAllowDataURI) { + mForceAllowDataURI = aForceAllowDataURI; +} + +bool nsDocShellLoadState::IsExemptFromHTTPSOnlyMode() const { + return mIsExemptFromHTTPSOnlyMode; +} + +void nsDocShellLoadState::SetIsExemptFromHTTPSOnlyMode( + bool aIsExemptFromHTTPSOnlyMode) { + mIsExemptFromHTTPSOnlyMode = aIsExemptFromHTTPSOnlyMode; +} + +bool nsDocShellLoadState::OriginalFrameSrc() const { return mOriginalFrameSrc; } + +void nsDocShellLoadState::SetOriginalFrameSrc(bool aOriginalFrameSrc) { + mOriginalFrameSrc = aOriginalFrameSrc; +} + +bool nsDocShellLoadState::IsFormSubmission() const { return mIsFormSubmission; } + +void nsDocShellLoadState::SetIsFormSubmission(bool aIsFormSubmission) { + mIsFormSubmission = aIsFormSubmission; +} + +uint32_t nsDocShellLoadState::LoadType() const { return mLoadType; } + +void nsDocShellLoadState::SetLoadType(uint32_t aLoadType) { + mLoadType = aLoadType; +} + +nsISHEntry* nsDocShellLoadState::SHEntry() const { return mSHEntry; } + +void nsDocShellLoadState::SetSHEntry(nsISHEntry* aSHEntry) { + mSHEntry = aSHEntry; + nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aSHEntry); + if (she) { + mLoadingSessionHistoryInfo = MakeUnique<LoadingSessionHistoryInfo>(she); + } else { + mLoadingSessionHistoryInfo = nullptr; + } +} + +void nsDocShellLoadState::SetLoadingSessionHistoryInfo( + const mozilla::dom::LoadingSessionHistoryInfo& aLoadingInfo) { + SetLoadingSessionHistoryInfo( + MakeUnique<mozilla::dom::LoadingSessionHistoryInfo>(aLoadingInfo)); +} + +void nsDocShellLoadState::SetLoadingSessionHistoryInfo( + mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> aLoadingInfo) { + mLoadingSessionHistoryInfo = std::move(aLoadingInfo); +} + +const mozilla::dom::LoadingSessionHistoryInfo* +nsDocShellLoadState::GetLoadingSessionHistoryInfo() const { + return mLoadingSessionHistoryInfo.get(); +} + +void nsDocShellLoadState::SetLoadIsFromSessionHistory( + int32_t aOffset, bool aLoadingCurrentEntry) { + if (mLoadingSessionHistoryInfo) { + mLoadingSessionHistoryInfo->mLoadIsFromSessionHistory = true; + mLoadingSessionHistoryInfo->mOffset = aOffset; + mLoadingSessionHistoryInfo->mLoadingCurrentEntry = aLoadingCurrentEntry; + } +} + +void nsDocShellLoadState::ClearLoadIsFromSessionHistory() { + if (mLoadingSessionHistoryInfo) { + mLoadingSessionHistoryInfo->mLoadIsFromSessionHistory = false; + } + mSHEntry = nullptr; +} + +bool nsDocShellLoadState::LoadIsFromSessionHistory() const { + return mLoadingSessionHistoryInfo + ? mLoadingSessionHistoryInfo->mLoadIsFromSessionHistory + : !!mSHEntry; +} + +void nsDocShellLoadState::MaybeStripTrackerQueryStrings( + BrowsingContext* aContext) { + MOZ_ASSERT(aContext); + + // Return early if the triggering principal doesn't exist. This could happen + // when loading a URL by using a browsing context in the Browser Toolbox. + if (!TriggeringPrincipal()) { + return; + } + + // We don't need to strip for sub frames because the query string has been + // stripped in the top-level content. Also, we don't apply stripping if it + // is triggered by addons. + // + // Note that we don't need to do the stripping if the channel has been + // initialized. This means that this has been loaded speculatively in the + // parent process before and the stripping was happening by then. + if (GetChannelInitialized() || !aContext->IsTopContent() || + BasePrincipal::Cast(TriggeringPrincipal())->AddonPolicy()) { + return; + } + + // We don't strip the URI if it's the same-site navigation. Note that we will + // consider the system principal triggered load as third-party in case the + // user copies and pastes a URL which has tracking query parameters or an + // loading from external applications, such as clicking a link in an email + // client. + bool isThirdPartyURI = false; + if (!TriggeringPrincipal()->IsSystemPrincipal() && + (NS_FAILED( + TriggeringPrincipal()->IsThirdPartyURI(URI(), &isThirdPartyURI)) || + !isThirdPartyURI)) { + return; + } + + Telemetry::AccumulateCategorical( + Telemetry::LABELS_QUERY_STRIPPING_COUNT::Navigation); + + nsCOMPtr<nsIURI> strippedURI; + + nsresult rv; + nsCOMPtr<nsIURLQueryStringStripper> queryStripper = + components::URLQueryStringStripper::Service(&rv); + NS_ENSURE_SUCCESS_VOID(rv); + + uint32_t numStripped; + + queryStripper->Strip(URI(), aContext->UsePrivateBrowsing(), + getter_AddRefs(strippedURI), &numStripped); + if (numStripped) { + if (!mUnstrippedURI) { + mUnstrippedURI = URI(); + } + SetURI(strippedURI); + + Telemetry::AccumulateCategorical( + Telemetry::LABELS_QUERY_STRIPPING_COUNT::StripForNavigation); + Telemetry::Accumulate(Telemetry::QUERY_STRIPPING_PARAM_COUNT, numStripped); + } + +#ifdef DEBUG + // Make sure that unstripped URI is the same as URI() but only the query + // string could be different. + if (mUnstrippedURI) { + nsCOMPtr<nsIURI> uri; + Unused << queryStripper->Strip(mUnstrippedURI, + aContext->UsePrivateBrowsing(), + getter_AddRefs(uri), &numStripped); + bool equals = false; + Unused << URI()->Equals(uri, &equals); + MOZ_ASSERT(equals); + } +#endif +} + +const nsString& nsDocShellLoadState::Target() const { return mTarget; } + +void nsDocShellLoadState::SetTarget(const nsAString& aTarget) { + mTarget = aTarget; +} + +nsIInputStream* nsDocShellLoadState::PostDataStream() const { + return mPostDataStream; +} + +void nsDocShellLoadState::SetPostDataStream(nsIInputStream* aStream) { + mPostDataStream = aStream; +} + +nsIInputStream* nsDocShellLoadState::HeadersStream() const { + return mHeadersStream; +} + +void nsDocShellLoadState::SetHeadersStream(nsIInputStream* aHeadersStream) { + mHeadersStream = aHeadersStream; +} + +const nsString& nsDocShellLoadState::SrcdocData() const { return mSrcdocData; } + +void nsDocShellLoadState::SetSrcdocData(const nsAString& aSrcdocData) { + mSrcdocData = aSrcdocData; +} + +void nsDocShellLoadState::SetSourceBrowsingContext( + BrowsingContext* aSourceBrowsingContext) { + mSourceBrowsingContext = aSourceBrowsingContext; +} + +void nsDocShellLoadState::SetTargetBrowsingContext( + BrowsingContext* aTargetBrowsingContext) { + mTargetBrowsingContext = aTargetBrowsingContext; +} + +nsIURI* nsDocShellLoadState::BaseURI() const { return mBaseURI; } + +void nsDocShellLoadState::SetBaseURI(nsIURI* aBaseURI) { mBaseURI = aBaseURI; } + +void nsDocShellLoadState::GetMaybeResultPrincipalURI( + mozilla::Maybe<nsCOMPtr<nsIURI>>& aRPURI) const { + bool isSome = ResultPrincipalURIIsSome(); + aRPURI.reset(); + + if (!isSome) { + return; + } + + nsCOMPtr<nsIURI> uri = ResultPrincipalURI(); + aRPURI.emplace(std::move(uri)); +} + +void nsDocShellLoadState::SetMaybeResultPrincipalURI( + mozilla::Maybe<nsCOMPtr<nsIURI>> const& aRPURI) { + SetResultPrincipalURI(aRPURI.refOr(nullptr)); + SetResultPrincipalURIIsSome(aRPURI.isSome()); +} + +uint32_t nsDocShellLoadState::LoadFlags() const { return mLoadFlags; } + +void nsDocShellLoadState::SetLoadFlags(uint32_t aLoadFlags) { + mLoadFlags = aLoadFlags; +} + +void nsDocShellLoadState::SetLoadFlag(uint32_t aFlag) { mLoadFlags |= aFlag; } + +void nsDocShellLoadState::UnsetLoadFlag(uint32_t aFlag) { + mLoadFlags &= ~aFlag; +} + +bool nsDocShellLoadState::HasLoadFlags(uint32_t aFlags) { + return (mLoadFlags & aFlags) == aFlags; +} + +uint32_t nsDocShellLoadState::InternalLoadFlags() const { + return mInternalLoadFlags; +} + +void nsDocShellLoadState::SetInternalLoadFlags(uint32_t aLoadFlags) { + mInternalLoadFlags = aLoadFlags; +} + +void nsDocShellLoadState::SetInternalLoadFlag(uint32_t aFlag) { + mInternalLoadFlags |= aFlag; +} + +void nsDocShellLoadState::UnsetInternalLoadFlag(uint32_t aFlag) { + mInternalLoadFlags &= ~aFlag; +} + +bool nsDocShellLoadState::HasInternalLoadFlags(uint32_t aFlags) { + return (mInternalLoadFlags & aFlags) == aFlags; +} + +bool nsDocShellLoadState::FirstParty() const { return mFirstParty; } + +void nsDocShellLoadState::SetFirstParty(bool aFirstParty) { + mFirstParty = aFirstParty; +} + +bool nsDocShellLoadState::HasValidUserGestureActivation() const { + return mHasValidUserGestureActivation; +} + +void nsDocShellLoadState::SetHasValidUserGestureActivation( + bool aHasValidUserGestureActivation) { + mHasValidUserGestureActivation = aHasValidUserGestureActivation; +} + +const nsCString& nsDocShellLoadState::TypeHint() const { return mTypeHint; } + +void nsDocShellLoadState::SetTypeHint(const nsCString& aTypeHint) { + mTypeHint = aTypeHint; +} + +const nsString& nsDocShellLoadState::FileName() const { return mFileName; } + +void nsDocShellLoadState::SetFileName(const nsAString& aFileName) { + MOZ_DIAGNOSTIC_ASSERT(aFileName.FindChar(char16_t(0)) == kNotFound, + "The filename should never contain null characters"); + mFileName = aFileName; +} + +const nsCString& nsDocShellLoadState::GetEffectiveTriggeringRemoteType() const { + // Consider non-errorpage loads from session history as being triggred by the + // parent process, as we'll validate them against the history entry. + // + // NOTE: Keep this check in-sync with the session-history validation check in + // `DocumentLoadListener::Open`! + if (LoadIsFromSessionHistory() && LoadType() != LOAD_ERROR_PAGE) { + return NOT_REMOTE_TYPE; + } + return mTriggeringRemoteType; +} + +void nsDocShellLoadState::SetTriggeringRemoteType( + const nsACString& aTriggeringRemoteType) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(), "only settable in parent"); + mTriggeringRemoteType = aTriggeringRemoteType; +} + +nsresult nsDocShellLoadState::SetupInheritingPrincipal( + BrowsingContext::Type aType, + const mozilla::OriginAttributes& aOriginAttributes) { + // We need a principalToInherit. + // + // If principalIsExplicit is not set there are 4 possibilities: + // (1) If the system principal or an expanded principal was passed + // in and we're a typeContent docshell, inherit the principal + // from the current document instead. + // (2) In all other cases when the principal passed in is not null, + // use that principal. + // (3) If the caller has allowed inheriting from the current document, + // or if we're being called from system code (eg chrome JS or pure + // C++) then inheritPrincipal should be true and InternalLoad will get + // a principal from the current document. If none of these things are + // true, then + // (4) we don't pass a principal into the channel, and a principal will be + // created later from the channel's internal data. + // + // If principalIsExplicit *is* set, there are 4 possibilities + // (1) If the system principal or an expanded principal was passed in + // and we're a typeContent docshell, return an error. + // (2) In all other cases when the principal passed in is not null, + // use that principal. + // (3) If the caller has allowed inheriting from the current document, + // then inheritPrincipal should be true and InternalLoad will get + // a principal from the current document. If none of these things are + // true, then + // (4) we dont' pass a principal into the channel, and a principal will be + // created later from the channel's internal data. + mPrincipalToInherit = mTriggeringPrincipal; + if (mPrincipalToInherit && aType != BrowsingContext::Type::Chrome) { + if (mPrincipalToInherit->IsSystemPrincipal()) { + if (mPrincipalIsExplicit) { + return NS_ERROR_DOM_SECURITY_ERR; + } + mPrincipalToInherit = nullptr; + mInheritPrincipal = true; + } else if (nsContentUtils::IsExpandedPrincipal(mPrincipalToInherit)) { + if (mPrincipalIsExplicit) { + return NS_ERROR_DOM_SECURITY_ERR; + } + // Don't inherit from the current page. Just do the safe thing + // and pretend that we were loaded by a nullprincipal. + // + // We didn't inherit OriginAttributes here as ExpandedPrincipal doesn't + // have origin attributes. + mPrincipalToInherit = NullPrincipal::Create(aOriginAttributes); + mInheritPrincipal = false; + } + } + + if (!mPrincipalToInherit && !mInheritPrincipal && !mPrincipalIsExplicit) { + // See if there's system or chrome JS code running + mInheritPrincipal = nsContentUtils::LegacyIsCallerChromeOrNativeCode(); + } + + if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL) { + mInheritPrincipal = false; + // Create a new null principal URI based on our precursor principal. + nsCOMPtr<nsIURI> nullPrincipalURI = + NullPrincipal::CreateURI(mPrincipalToInherit); + // If mFirstParty is true and the pref 'privacy.firstparty.isolate' is + // enabled, we will set firstPartyDomain on the origin attributes. + OriginAttributes attrs(aOriginAttributes); + if (mFirstParty) { + attrs.SetFirstPartyDomain(true, nullPrincipalURI); + } + mPrincipalToInherit = NullPrincipal::Create(attrs, nullPrincipalURI); + } + + return NS_OK; +} + +nsresult nsDocShellLoadState::SetupTriggeringPrincipal( + const mozilla::OriginAttributes& aOriginAttributes) { + // If the triggeringPrincipal is not set, we first try to create a principal + // from the referrer, since the referrer URI reflects the web origin that + // triggered the load. If there is no referrer URI, we fall back to using the + // SystemPrincipal. It's safe to assume that no provided triggeringPrincipal + // and no referrer simulate a load that was triggered by the system. It's + // important to note that this block of code needs to appear *after* the block + // where we munge the principalToInherit, because otherwise we would never + // enter code blocks checking if the principalToInherit is null and we will + // end up with a wrong inheritPrincipal flag. + if (!mTriggeringPrincipal) { + if (mReferrerInfo) { + nsCOMPtr<nsIURI> referrer = mReferrerInfo->GetOriginalReferrer(); + mTriggeringPrincipal = + BasePrincipal::CreateContentPrincipal(referrer, aOriginAttributes); + + if (!mTriggeringPrincipal) { + return NS_ERROR_FAILURE; + } + } else { + mTriggeringPrincipal = nsContentUtils::GetSystemPrincipal(); + } + } + return NS_OK; +} + +void nsDocShellLoadState::CalculateLoadURIFlags() { + if (mInheritPrincipal) { + MOZ_ASSERT( + !mPrincipalToInherit || !mPrincipalToInherit->IsSystemPrincipal(), + "Should not inherit SystemPrincipal"); + mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_INHERIT_PRINCIPAL; + } + + if (mReferrerInfo && !mReferrerInfo->GetSendReferrer()) { + mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER; + } + if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP) { + mInternalLoadFlags |= + nsDocShell::INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + } + + if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD) { + mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_FIRST_LOAD; + } + + if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_CLASSIFIER) { + mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_BYPASS_CLASSIFIER; + } + + if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_FORCE_ALLOW_COOKIES) { + mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_FORCE_ALLOW_COOKIES; + } + + if (mLoadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE) { + mInternalLoadFlags |= + nsDocShell::INTERNAL_LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE; + } + + if (!mSrcdocData.IsVoid()) { + mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_IS_SRCDOC; + } + + if (mForceAllowDataURI) { + mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_FORCE_ALLOW_DATA_URI; + } + + if (mOriginalFrameSrc) { + mInternalLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_ORIGINAL_FRAME_SRC; + } +} + +nsLoadFlags nsDocShellLoadState::CalculateChannelLoadFlags( + BrowsingContext* aBrowsingContext, Maybe<bool> aUriModified, + Maybe<bool> aIsXFOError) { + MOZ_ASSERT(aBrowsingContext); + + nsLoadFlags loadFlags = aBrowsingContext->GetDefaultLoadFlags(); + + if (FirstParty()) { + // tag first party URL loads + loadFlags |= nsIChannel::LOAD_INITIAL_DOCUMENT_URI; + } + + const uint32_t loadType = LoadType(); + + // These values aren't available for loads initiated in the Parent process. + MOZ_ASSERT_IF(loadType == LOAD_HISTORY, aUriModified.isSome()); + MOZ_ASSERT_IF(loadType == LOAD_ERROR_PAGE, aIsXFOError.isSome()); + + if (loadType == LOAD_ERROR_PAGE) { + // Error pages are LOAD_BACKGROUND, unless it's an + // XFO error for which we want an error page to load + // but additionally want the onload() event to fire. + if (!*aIsXFOError) { + loadFlags |= nsIChannel::LOAD_BACKGROUND; + } + } + + // Mark the channel as being a document URI and allow content sniffing... + loadFlags |= + nsIChannel::LOAD_DOCUMENT_URI | nsIChannel::LOAD_CALL_CONTENT_SNIFFERS; + + if (nsDocShell::SandboxFlagsImplyCookies( + aBrowsingContext->GetSandboxFlags())) { + loadFlags |= nsIRequest::LOAD_DOCUMENT_NEEDS_COOKIE; + } + + // Load attributes depend on load type... + switch (loadType) { + case LOAD_HISTORY: { + // Only send VALIDATE_NEVER if mLSHE's URI was never changed via + // push/replaceState (bug 669671). + if (!*aUriModified) { + loadFlags |= nsIRequest::VALIDATE_NEVER; + } + break; + } + + case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE: + case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE: + loadFlags |= + nsIRequest::LOAD_BYPASS_CACHE | nsIRequest::LOAD_FRESH_CONNECTION; + [[fallthrough]]; + + case LOAD_REFRESH: + loadFlags |= nsIRequest::VALIDATE_ALWAYS; + break; + + case LOAD_NORMAL_BYPASS_CACHE: + case LOAD_NORMAL_BYPASS_PROXY: + case LOAD_NORMAL_BYPASS_PROXY_AND_CACHE: + case LOAD_RELOAD_BYPASS_CACHE: + case LOAD_RELOAD_BYPASS_PROXY: + case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE: + case LOAD_REPLACE_BYPASS_CACHE: + loadFlags |= + nsIRequest::LOAD_BYPASS_CACHE | nsIRequest::LOAD_FRESH_CONNECTION; + break; + + case LOAD_RELOAD_NORMAL: + if (!StaticPrefs:: + browser_soft_reload_only_force_validate_top_level_document()) { + loadFlags |= nsIRequest::VALIDATE_ALWAYS; + break; + } + [[fallthrough]]; + case LOAD_NORMAL: + case LOAD_LINK: + // Set cache checking flags + switch (StaticPrefs::browser_cache_check_doc_frequency()) { + case 0: + loadFlags |= nsIRequest::VALIDATE_ONCE_PER_SESSION; + break; + case 1: + loadFlags |= nsIRequest::VALIDATE_ALWAYS; + break; + case 2: + loadFlags |= nsIRequest::VALIDATE_NEVER; + break; + } + break; + } + + if (HasInternalLoadFlags(nsDocShell::INTERNAL_LOAD_FLAGS_BYPASS_CLASSIFIER)) { + loadFlags |= nsIChannel::LOAD_BYPASS_URL_CLASSIFIER; + } + + // If the user pressed shift-reload, then do not allow ServiceWorker + // interception to occur. See step 12.1 of the SW HandleFetch algorithm. + if (IsForceReloadType(loadType)) { + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + } + + return loadFlags; +} + +const char* nsDocShellLoadState::ValidateWithOriginalState( + nsDocShellLoadState* aOriginalState) { + MOZ_ASSERT(mLoadIdentifier == aOriginalState->mLoadIdentifier); + + // Check that `aOriginalState` is sufficiently similar to this state that + // they're performing the same load. + auto uriEq = [](nsIURI* a, nsIURI* b) -> bool { + bool eq = false; + return a == b || (a && b && NS_SUCCEEDED(a->Equals(b, &eq)) && eq); + }; + if (!uriEq(mURI, aOriginalState->mURI)) { + return "URI"; + } + if (!uriEq(mUnstrippedURI, aOriginalState->mUnstrippedURI)) { + return "UnstrippedURI"; + } + if (!uriEq(mOriginalURI, aOriginalState->mOriginalURI)) { + return "OriginalURI"; + } + if (!uriEq(mBaseURI, aOriginalState->mBaseURI)) { + return "BaseURI"; + } + + if (!mTriggeringPrincipal->Equals(aOriginalState->mTriggeringPrincipal)) { + return "TriggeringPrincipal"; + } + if (mTriggeringSandboxFlags != aOriginalState->mTriggeringSandboxFlags) { + return "TriggeringSandboxFlags"; + } + if (mTriggeringRemoteType != aOriginalState->mTriggeringRemoteType) { + return "TriggeringRemoteType"; + } + + if (mOriginalURIString != aOriginalState->mOriginalURIString) { + return "OriginalURIString"; + } + + if (mRemoteTypeOverride != aOriginalState->mRemoteTypeOverride) { + return "RemoteTypeOverride"; + } + + if (mSourceBrowsingContext.ContextId() != + aOriginalState->mSourceBrowsingContext.ContextId()) { + return "SourceBrowsingContext"; + } + + // FIXME: Consider calculating less information in the target process so that + // we can validate more properties more easily. + // FIXME: Identify what other flags will not change when sent through a + // content process. + + return nullptr; +} + +DocShellLoadStateInit nsDocShellLoadState::Serialize( + mozilla::ipc::IProtocol* aActor) { + MOZ_ASSERT(aActor); + DocShellLoadStateInit loadState; + loadState.ResultPrincipalURI() = mResultPrincipalURI; + loadState.ResultPrincipalURIIsSome() = mResultPrincipalURIIsSome; + loadState.KeepResultPrincipalURIIfSet() = mKeepResultPrincipalURIIfSet; + loadState.LoadReplace() = mLoadReplace; + loadState.InheritPrincipal() = mInheritPrincipal; + loadState.PrincipalIsExplicit() = mPrincipalIsExplicit; + loadState.ForceAllowDataURI() = mForceAllowDataURI; + loadState.IsExemptFromHTTPSOnlyMode() = mIsExemptFromHTTPSOnlyMode; + loadState.OriginalFrameSrc() = mOriginalFrameSrc; + loadState.IsFormSubmission() = mIsFormSubmission; + loadState.LoadType() = mLoadType; + loadState.Target() = mTarget; + loadState.TargetBrowsingContext() = mTargetBrowsingContext; + loadState.LoadFlags() = mLoadFlags; + loadState.InternalLoadFlags() = mInternalLoadFlags; + loadState.FirstParty() = mFirstParty; + loadState.HasValidUserGestureActivation() = mHasValidUserGestureActivation; + loadState.AllowFocusMove() = mAllowFocusMove; + loadState.TypeHint() = mTypeHint; + loadState.FileName() = mFileName; + loadState.IsFromProcessingFrameAttributes() = + mIsFromProcessingFrameAttributes; + loadState.URI() = mURI; + loadState.OriginalURI() = mOriginalURI; + loadState.SourceBrowsingContext() = mSourceBrowsingContext; + loadState.BaseURI() = mBaseURI; + loadState.TriggeringPrincipal() = mTriggeringPrincipal; + loadState.PrincipalToInherit() = mPrincipalToInherit; + loadState.PartitionedPrincipalToInherit() = mPartitionedPrincipalToInherit; + loadState.TriggeringSandboxFlags() = mTriggeringSandboxFlags; + loadState.TriggeringRemoteType() = mTriggeringRemoteType; + loadState.Csp() = mCsp; + loadState.OriginalURIString() = mOriginalURIString; + loadState.CancelContentJSEpoch() = mCancelContentJSEpoch; + loadState.ReferrerInfo() = mReferrerInfo; + loadState.PostDataStream() = mPostDataStream; + loadState.HeadersStream() = mHeadersStream; + loadState.SrcdocData() = mSrcdocData; + loadState.ResultPrincipalURI() = mResultPrincipalURI; + loadState.LoadIdentifier() = mLoadIdentifier; + loadState.ChannelInitialized() = mChannelInitialized; + loadState.IsMetaRefresh() = mIsMetaRefresh; + if (mLoadingSessionHistoryInfo) { + loadState.loadingSessionHistoryInfo().emplace(*mLoadingSessionHistoryInfo); + } + loadState.UnstrippedURI() = mUnstrippedURI; + loadState.RemoteTypeOverride() = mRemoteTypeOverride; + + if (XRE_IsParentProcess()) { + mozilla::ipc::IToplevelProtocol* top = aActor->ToplevelProtocol(); + MOZ_RELEASE_ASSERT(top && + top->GetProtocolId() == + mozilla::ipc::ProtocolId::PContentMsgStart && + top->GetSide() == mozilla::ipc::ParentSide, + "nsDocShellLoadState must be sent over PContent"); + ContentParent* cp = static_cast<ContentParent*>(top); + cp->StorePendingLoadState(this); + } + + return loadState; +} + +nsIURI* nsDocShellLoadState::GetUnstrippedURI() const { return mUnstrippedURI; } + +void nsDocShellLoadState::SetUnstrippedURI(nsIURI* aUnstrippedURI) { + mUnstrippedURI = aUnstrippedURI; +} diff --git a/docshell/base/nsDocShellLoadState.h b/docshell/base/nsDocShellLoadState.h new file mode 100644 index 0000000000..f74e4f24fb --- /dev/null +++ b/docshell/base/nsDocShellLoadState.h @@ -0,0 +1,573 @@ +/* -*- 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/. */ + +#ifndef nsDocShellLoadState_h__ +#define nsDocShellLoadState_h__ + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/SessionHistoryEntry.h" + +// Helper Classes +#include "mozilla/Maybe.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsDocShellLoadTypes.h" +#include "nsTArrayForwardDeclare.h" + +class nsIContentSecurityPolicy; +class nsIInputStream; +class nsISHEntry; +class nsIURI; +class nsIDocShell; +class nsIChannel; +class nsIReferrerInfo; +namespace mozilla { +class OriginAttributes; +template <typename, class> +class UniquePtr; +namespace dom { +class DocShellLoadStateInit; +} // namespace dom +} // namespace mozilla + +/** + * nsDocShellLoadState contains setup information used in a nsIDocShell::loadURI + * call. + */ +class nsDocShellLoadState final { + using BrowsingContext = mozilla::dom::BrowsingContext; + template <typename T> + using MaybeDiscarded = mozilla::dom::MaybeDiscarded<T>; + + public: + NS_INLINE_DECL_REFCOUNTING(nsDocShellLoadState); + + explicit nsDocShellLoadState(nsIURI* aURI); + explicit nsDocShellLoadState( + const mozilla::dom::DocShellLoadStateInit& aLoadState, + mozilla::ipc::IProtocol* aActor, bool* aReadSuccess); + explicit nsDocShellLoadState(const nsDocShellLoadState& aOther); + nsDocShellLoadState(nsIURI* aURI, uint64_t aLoadIdentifier); + + static nsresult CreateFromPendingChannel(nsIChannel* aPendingChannel, + uint64_t aLoadIdentifier, + uint64_t aRegistarId, + nsDocShellLoadState** aResult); + + static nsresult CreateFromLoadURIOptions( + BrowsingContext* aBrowsingContext, const nsAString& aURI, + const mozilla::dom::LoadURIOptions& aLoadURIOptions, + nsDocShellLoadState** aResult); + static nsresult CreateFromLoadURIOptions( + BrowsingContext* aBrowsingContext, nsIURI* aURI, + const mozilla::dom::LoadURIOptions& aLoadURIOptions, + nsDocShellLoadState** aResult); + + // Getters and Setters + + nsIReferrerInfo* GetReferrerInfo() const; + + void SetReferrerInfo(nsIReferrerInfo* aReferrerInfo); + + nsIURI* URI() const; + + void SetURI(nsIURI* aURI); + + nsIURI* OriginalURI() const; + + void SetOriginalURI(nsIURI* aOriginalURI); + + nsIURI* ResultPrincipalURI() const; + + void SetResultPrincipalURI(nsIURI* aResultPrincipalURI); + + bool ResultPrincipalURIIsSome() const; + + void SetResultPrincipalURIIsSome(bool aIsSome); + + bool KeepResultPrincipalURIIfSet() const; + + void SetKeepResultPrincipalURIIfSet(bool aKeep); + + nsIPrincipal* PrincipalToInherit() const; + + void SetPrincipalToInherit(nsIPrincipal* aPrincipalToInherit); + + nsIPrincipal* PartitionedPrincipalToInherit() const; + + void SetPartitionedPrincipalToInherit( + nsIPrincipal* aPartitionedPrincipalToInherit); + + bool LoadReplace() const; + + void SetLoadReplace(bool aLoadReplace); + + nsIPrincipal* TriggeringPrincipal() const; + + void SetTriggeringPrincipal(nsIPrincipal* aTriggeringPrincipal); + + uint32_t TriggeringSandboxFlags() const; + + void SetTriggeringSandboxFlags(uint32_t aTriggeringSandboxFlags); + + nsIContentSecurityPolicy* Csp() const; + + void SetCsp(nsIContentSecurityPolicy* aCsp); + + bool InheritPrincipal() const; + + void SetInheritPrincipal(bool aInheritPrincipal); + + bool PrincipalIsExplicit() const; + + void SetPrincipalIsExplicit(bool aPrincipalIsExplicit); + + // If true, "beforeunload" event listeners were notified by the creater of the + // LoadState and given the chance to abort the navigation, and should not be + // notified again. + bool NotifiedBeforeUnloadListeners() const; + + void SetNotifiedBeforeUnloadListeners(bool aNotifiedBeforeUnloadListeners); + + bool ForceAllowDataURI() const; + + void SetForceAllowDataURI(bool aForceAllowDataURI); + + bool IsExemptFromHTTPSOnlyMode() const; + + void SetIsExemptFromHTTPSOnlyMode(bool aIsExemptFromHTTPSOnlyMode); + + bool OriginalFrameSrc() const; + + void SetOriginalFrameSrc(bool aOriginalFrameSrc); + + bool IsFormSubmission() const; + + void SetIsFormSubmission(bool aIsFormSubmission); + + uint32_t LoadType() const; + + void SetLoadType(uint32_t aLoadType); + + nsISHEntry* SHEntry() const; + + void SetSHEntry(nsISHEntry* aSHEntry); + + const mozilla::dom::LoadingSessionHistoryInfo* GetLoadingSessionHistoryInfo() + const; + + // Copies aLoadingInfo and stores the copy in this nsDocShellLoadState. + void SetLoadingSessionHistoryInfo( + const mozilla::dom::LoadingSessionHistoryInfo& aLoadingInfo); + + // Stores aLoadingInfo in this nsDocShellLoadState. + void SetLoadingSessionHistoryInfo( + mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> aLoadingInfo); + + bool LoadIsFromSessionHistory() const; + + const nsString& Target() const; + + void SetTarget(const nsAString& aTarget); + + nsIInputStream* PostDataStream() const; + + void SetPostDataStream(nsIInputStream* aStream); + + nsIInputStream* HeadersStream() const; + + void SetHeadersStream(nsIInputStream* aHeadersStream); + + bool IsSrcdocLoad() const; + + const nsString& SrcdocData() const; + + void SetSrcdocData(const nsAString& aSrcdocData); + + const MaybeDiscarded<BrowsingContext>& SourceBrowsingContext() const { + return mSourceBrowsingContext; + } + + void SetSourceBrowsingContext(BrowsingContext*); + + void SetAllowFocusMove(bool aAllow) { mAllowFocusMove = aAllow; } + + bool AllowFocusMove() const { return mAllowFocusMove; } + + const MaybeDiscarded<BrowsingContext>& TargetBrowsingContext() const { + return mTargetBrowsingContext; + } + + void SetTargetBrowsingContext(BrowsingContext* aTargetBrowsingContext); + + nsIURI* BaseURI() const; + + void SetBaseURI(nsIURI* aBaseURI); + + // Helper function allowing convenient work with mozilla::Maybe in C++, hiding + // resultPrincipalURI and resultPrincipalURIIsSome attributes from the + // consumer. + void GetMaybeResultPrincipalURI( + mozilla::Maybe<nsCOMPtr<nsIURI>>& aRPURI) const; + + void SetMaybeResultPrincipalURI( + mozilla::Maybe<nsCOMPtr<nsIURI>> const& aRPURI); + + uint32_t LoadFlags() const; + + void SetLoadFlags(uint32_t aFlags); + + void SetLoadFlag(uint32_t aFlag); + + void UnsetLoadFlag(uint32_t aFlag); + + bool HasLoadFlags(uint32_t aFlag); + + uint32_t InternalLoadFlags() const; + + void SetInternalLoadFlags(uint32_t aFlags); + + void SetInternalLoadFlag(uint32_t aFlag); + + void UnsetInternalLoadFlag(uint32_t aFlag); + + bool HasInternalLoadFlags(uint32_t aFlag); + + bool FirstParty() const; + + void SetFirstParty(bool aFirstParty); + + bool HasValidUserGestureActivation() const; + + void SetHasValidUserGestureActivation(bool HasValidUserGestureActivation); + + const nsCString& TypeHint() const; + + void SetTypeHint(const nsCString& aTypeHint); + + const nsString& FileName() const; + + void SetFileName(const nsAString& aFileName); + + nsIURI* GetUnstrippedURI() const; + + void SetUnstrippedURI(nsIURI* aUnstrippedURI); + + // Give the type of DocShell we're loading into (chrome/content/etc) and + // origin attributes for the URI we're loading, figure out if we should + // inherit our principal from the document the load was requested from, or + // else if the principal should be set up later in the process (after loads). + // See comments in function for more info on principal selection algorithm + nsresult SetupInheritingPrincipal( + mozilla::dom::BrowsingContext::Type aType, + const mozilla::OriginAttributes& aOriginAttributes); + + // If no triggering principal exists at the moment, create one using referrer + // information and origin attributes. + nsresult SetupTriggeringPrincipal( + const mozilla::OriginAttributes& aOriginAttributes); + + void SetIsFromProcessingFrameAttributes() { + mIsFromProcessingFrameAttributes = true; + } + bool GetIsFromProcessingFrameAttributes() const { + return mIsFromProcessingFrameAttributes; + } + + nsIChannel* GetPendingRedirectedChannel() { + return mPendingRedirectedChannel; + } + + uint64_t GetPendingRedirectChannelRegistrarId() const { + return mChannelRegistrarId; + } + + void SetOriginalURIString(const nsCString& aOriginalURI) { + mOriginalURIString.emplace(aOriginalURI); + } + const mozilla::Maybe<nsCString>& GetOriginalURIString() const { + return mOriginalURIString; + } + + void SetCancelContentJSEpoch(int32_t aCancelEpoch) { + mCancelContentJSEpoch.emplace(aCancelEpoch); + } + const mozilla::Maybe<int32_t>& GetCancelContentJSEpoch() const { + return mCancelContentJSEpoch; + } + + uint64_t GetLoadIdentifier() const { return mLoadIdentifier; } + + void SetChannelInitialized(bool aInitilized) { + mChannelInitialized = aInitilized; + } + + bool GetChannelInitialized() const { return mChannelInitialized; } + + void SetIsMetaRefresh(bool aMetaRefresh) { mIsMetaRefresh = aMetaRefresh; } + + bool IsMetaRefresh() const { return mIsMetaRefresh; } + + const mozilla::Maybe<nsCString>& GetRemoteTypeOverride() const { + return mRemoteTypeOverride; + } + + void SetRemoteTypeOverride(const nsCString& aRemoteTypeOverride) { + mRemoteTypeOverride = mozilla::Some(aRemoteTypeOverride); + } + + // Determine the remote type of the process which should be considered + // responsible for this load for the purposes of security checks. + // + // This will generally be the process which created the nsDocShellLoadState + // originally, however non-errorpage history loads are always considered to be + // triggered by the parent process, as we can validate them against the + // history entry. + const nsCString& GetEffectiveTriggeringRemoteType() const; + + void SetTriggeringRemoteType(const nsACString& aTriggeringRemoteType); + + // When loading a document through nsDocShell::LoadURI(), a special set of + // flags needs to be set based on other values in nsDocShellLoadState. This + // function calculates those flags, before the LoadState is passed to + // nsDocShell::InternalLoad. + void CalculateLoadURIFlags(); + + // Compute the load flags to be used by creating channel. aUriModified and + // aIsXFOError are expected to be Nothing when called from Parent process. + nsLoadFlags CalculateChannelLoadFlags( + mozilla::dom::BrowsingContext* aBrowsingContext, + mozilla::Maybe<bool> aUriModified, mozilla::Maybe<bool> aIsXFOError); + + mozilla::dom::DocShellLoadStateInit Serialize( + mozilla::ipc::IProtocol* aActor); + + void SetLoadIsFromSessionHistory(int32_t aOffset, bool aLoadingCurrentEntry); + void ClearLoadIsFromSessionHistory(); + + void MaybeStripTrackerQueryStrings(mozilla::dom::BrowsingContext* aContext); + + protected: + // Destructor can't be defaulted or inlined, as header doesn't have all type + // includes it needs to do so. + ~nsDocShellLoadState(); + + // Given the original `nsDocShellLoadState` which was sent to a content + // process, validate that they corespond to the same load. + // Returns a static (telemetry-safe) string naming what did not match, or + // nullptr if it succeeds. + const char* ValidateWithOriginalState(nsDocShellLoadState* aOriginalState); + + static nsresult CreateFromLoadURIOptions( + BrowsingContext* aBrowsingContext, nsIURI* aURI, + const mozilla::dom::LoadURIOptions& aLoadURIOptions, + uint32_t aLoadFlagsOverride, nsIInputStream* aPostDataOverride, + nsDocShellLoadState** aResult); + + // This is the referrer for the load. + nsCOMPtr<nsIReferrerInfo> mReferrerInfo; + + // The URI we are navigating to. Will not be null once set. + nsCOMPtr<nsIURI> mURI; + + // The URI to set as the originalURI on the channel that does the load. If + // null, aURI will be set as the originalURI. + nsCOMPtr<nsIURI> mOriginalURI; + + // The URI to be set to loadInfo.resultPrincipalURI + // - When Nothing, there will be no change + // - When Some, the principal URI will overwrite even + // with a null value. + // + // Valid only if mResultPrincipalURIIsSome is true (has the same meaning as + // isSome() on mozilla::Maybe.) + nsCOMPtr<nsIURI> mResultPrincipalURI; + bool mResultPrincipalURIIsSome; + + // The principal of the load, that is, the entity responsible for causing the + // load to occur. In most cases the referrer and the triggeringPrincipal's URI + // will be identical. + // + // Please note that this is the principal that is used for security checks. If + // the argument aURI is provided by the web, then please do not pass a + // SystemPrincipal as the triggeringPrincipal. + nsCOMPtr<nsIPrincipal> mTriggeringPrincipal; + + // The SandboxFlags of the load, that are, the SandboxFlags of the entity + // responsible for causing the load to occur. Most likely this are the + // SandboxFlags of the document that started the load. + uint32_t mTriggeringSandboxFlags; + + // The CSP of the load, that is, the CSP of the entity responsible for causing + // the load to occur. Most likely this is the CSP of the document that started + // the load. In case the entity starting the load did not use a CSP, then mCsp + // can be null. Please note that this is also the CSP that will be applied to + // the load in case the load encounters a server side redirect. + nsCOMPtr<nsIContentSecurityPolicy> mCsp; + + // If a refresh is caused by http-equiv="refresh" we want to set + // aResultPrincipalURI, but we do not want to overwrite the channel's + // ResultPrincipalURI, if it has already been set on the channel by a protocol + // handler. + bool mKeepResultPrincipalURIIfSet; + + // If set LOAD_REPLACE flag will be set on the channel. If aOriginalURI is + // null, this argument is ignored. + bool mLoadReplace; + + // If this attribute is true and no triggeringPrincipal is specified, + // copy the principal from the referring document. + bool mInheritPrincipal; + + // If this attribute is true only ever use the principal specified + // by the triggeringPrincipal and inheritPrincipal attributes. + // If there are security reasons for why this is unsafe, such + // as trying to use a systemprincipal as the triggeringPrincipal + // for a content docshell the load fails. + bool mPrincipalIsExplicit; + + bool mNotifiedBeforeUnloadListeners; + + // Principal we're inheriting. If null, this means the principal should be + // inherited from the current document. If set to NullPrincipal, the channel + // will fill in principal information later in the load. See internal comments + // of SetupInheritingPrincipal for more info. + // + // When passed to InternalLoad, If this argument is null then + // principalToInherit is computed differently. See nsDocShell::InternalLoad + // for more comments. + + nsCOMPtr<nsIPrincipal> mPrincipalToInherit; + + nsCOMPtr<nsIPrincipal> mPartitionedPrincipalToInherit; + + // If this attribute is true, then a top-level navigation + // to a data URI will be allowed. + bool mForceAllowDataURI; + + // If this attribute is true, then the top-level navigaion + // will be exempt from HTTPS-Only-Mode upgrades. + bool mIsExemptFromHTTPSOnlyMode; + + // If this attribute is true, this load corresponds to a frame + // element loading its original src (or srcdoc) attribute. + bool mOriginalFrameSrc; + + // If this attribute is true, then the load was initiated by a + // form submission. This is important to know for the CSP directive + // navigate-to. + bool mIsFormSubmission; + + // Contains a load type as specified by the nsDocShellLoadTypes::load* + // constants + uint32_t mLoadType; + + // Active Session History entry (if loading from SH) + nsCOMPtr<nsISHEntry> mSHEntry; + + // Loading session history info for the load + mozilla::UniquePtr<mozilla::dom::LoadingSessionHistoryInfo> + mLoadingSessionHistoryInfo; + + // Target for load, like _content, _blank etc. + nsString mTarget; + + // When set, this is the Target Browsing Context for the navigation + // after retargeting. + MaybeDiscarded<BrowsingContext> mTargetBrowsingContext; + + // Post data stream (if POSTing) + nsCOMPtr<nsIInputStream> mPostDataStream; + + // Additional Headers + nsCOMPtr<nsIInputStream> mHeadersStream; + + // When set, the load will be interpreted as a srcdoc load, where contents of + // this string will be loaded instead of the URI. Setting srcdocData sets + // isSrcdocLoad to true + nsString mSrcdocData; + + // When set, this is the Source Browsing Context for the navigation. + MaybeDiscarded<BrowsingContext> mSourceBrowsingContext; + + // Used for srcdoc loads to give view-source knowledge of the load's base URI + // as this information isn't embedded in the load's URI. + nsCOMPtr<nsIURI> mBaseURI; + + // Set of Load Flags, taken from nsDocShellLoadTypes.h and nsIWebNavigation + uint32_t mLoadFlags; + + // Set of internal load flags + uint32_t mInternalLoadFlags; + + // Is this a First Party Load? + bool mFirstParty; + + // Is this load triggered by a user gesture? + bool mHasValidUserGestureActivation; + + // Whether this load can steal the focus from the source browsing context. + bool mAllowFocusMove; + + // A hint as to the content-type of the resulting data. If no hint, IsVoid() + // should return true. + nsCString mTypeHint; + + // Non-void when the link should be downloaded as the given filename. + // mFileName being non-void but empty means that no filename hint was + // specified, but link should still trigger a download. If not a download, + // mFileName.IsVoid() should return true. + nsString mFileName; + + // This will be true if this load is triggered by attribute changes. + // See nsILoadInfo.isFromProcessingFrameAttributes + bool mIsFromProcessingFrameAttributes; + + // If set, a pending cross-process redirected channel should be used to + // perform the load. The channel will be stored in this value. + nsCOMPtr<nsIChannel> mPendingRedirectedChannel; + + // An optional string representation of mURI, before any + // fixups were applied, so that we can send it to a search + // engine service if needed. + mozilla::Maybe<nsCString> mOriginalURIString; + + // An optional value to pass to nsIDocShell::setCancelJSEpoch + // when initiating the load. + mozilla::Maybe<int32_t> mCancelContentJSEpoch; + + // If mPendingRedirectChannel is set, then this is the identifier + // that the parent-process equivalent channel has been registered + // with using RedirectChannelRegistrar. + uint64_t mChannelRegistrarId; + + // An identifier to make it possible to examine if two loads are + // equal, and which browsing context they belong to (see + // BrowsingContext::{Get, Set}CurrentLoadIdentifier) + const uint64_t mLoadIdentifier; + + // Optional value to indicate that a channel has been + // pre-initialized in the parent process. + bool mChannelInitialized; + + // True if the load was triggered by a meta refresh. + bool mIsMetaRefresh; + + // True if the nsDocShellLoadState was received over IPC. + bool mWasCreatedRemotely = false; + + // The original URI before query stripping happened. If it's present, it shows + // the query stripping happened. Otherwise, it will be a nullptr. + nsCOMPtr<nsIURI> mUnstrippedURI; + + // If set, the remote type which the load should be completed within. + mozilla::Maybe<nsCString> mRemoteTypeOverride; + + // Remote type of the process which originally requested the load. + nsCString mTriggeringRemoteType; +}; + +#endif /* nsDocShellLoadState_h__ */ diff --git a/docshell/base/nsDocShellLoadTypes.h b/docshell/base/nsDocShellLoadTypes.h new file mode 100644 index 0000000000..1de19e81eb --- /dev/null +++ b/docshell/base/nsDocShellLoadTypes.h @@ -0,0 +1,205 @@ +/* -*- 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/. */ + +#ifndef nsDocShellLoadTypes_h_ +#define nsDocShellLoadTypes_h_ + +#ifdef MOZILLA_INTERNAL_API + +# include "nsDOMNavigationTiming.h" +# include "nsIDocShell.h" +# include "nsIWebNavigation.h" + +/** + * Load flag for error pages. This uses one of the reserved flag + * values from nsIWebNavigation. + */ +# define LOAD_FLAGS_ERROR_PAGE 0x0001U + +# define MAKE_LOAD_TYPE(type, flags) ((type) | ((flags) << 16)) +# define LOAD_TYPE_HAS_FLAGS(type, flags) ((type) & ((flags) << 16)) + +/** + * These are flags that confuse ConvertLoadTypeToDocShellLoadInfo and should + * not be passed to MAKE_LOAD_TYPE. In particular this includes all flags + * above 0xffff (e.g. LOAD_FLAGS_BYPASS_CLASSIFIER), since MAKE_LOAD_TYPE would + * just shift them out anyway. + */ +# define EXTRA_LOAD_FLAGS \ + (nsIWebNavigation::LOAD_FLAGS_FROM_EXTERNAL | \ + nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD | \ + nsIWebNavigation::LOAD_FLAGS_ALLOW_POPUPS | 0xffff0000) + +/* load types are legal combinations of load commands and flags + * + * NOTE: + * Remember to update the IsValidLoadType function below if you change this + * enum to ensure bad flag combinations will be rejected. + */ +enum LoadType : uint32_t { + LOAD_NORMAL = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_NONE), + LOAD_NORMAL_REPLACE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY), + LOAD_HISTORY = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_HISTORY, + nsIWebNavigation::LOAD_FLAGS_NONE), + LOAD_NORMAL_BYPASS_CACHE = MAKE_LOAD_TYPE( + nsIDocShell::LOAD_CMD_NORMAL, nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE), + LOAD_NORMAL_BYPASS_PROXY = MAKE_LOAD_TYPE( + nsIDocShell::LOAD_CMD_NORMAL, nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY), + LOAD_NORMAL_BYPASS_PROXY_AND_CACHE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE | + nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY), + LOAD_RELOAD_NORMAL = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD, + nsIWebNavigation::LOAD_FLAGS_NONE), + LOAD_RELOAD_BYPASS_CACHE = MAKE_LOAD_TYPE( + nsIDocShell::LOAD_CMD_RELOAD, nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE), + LOAD_RELOAD_BYPASS_PROXY = MAKE_LOAD_TYPE( + nsIDocShell::LOAD_CMD_RELOAD, nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY), + LOAD_RELOAD_BYPASS_PROXY_AND_CACHE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD, + nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE | + nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY), + LOAD_LINK = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_IS_LINK), + LOAD_REFRESH = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_IS_REFRESH), + LOAD_REFRESH_REPLACE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_IS_REFRESH | + nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY), + LOAD_RELOAD_CHARSET_CHANGE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD, + nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE), + LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD, + nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE | + nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE | + nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY), + LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_RELOAD, + nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE | + nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE), + LOAD_BYPASS_HISTORY = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_BYPASS_HISTORY), + LOAD_STOP_CONTENT = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_STOP_CONTENT), + LOAD_STOP_CONTENT_AND_REPLACE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_STOP_CONTENT | + nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY), + LOAD_PUSHSTATE = MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_PUSHSTATE, + nsIWebNavigation::LOAD_FLAGS_NONE), + LOAD_REPLACE_BYPASS_CACHE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, + nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY | + nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE), + /** + * Load type for an error page. These loads are never triggered by users of + * Docshell. Instead, Docshell triggers the load itself when a + * consumer-triggered load failed. + */ + LOAD_ERROR_PAGE = + MAKE_LOAD_TYPE(nsIDocShell::LOAD_CMD_NORMAL, LOAD_FLAGS_ERROR_PAGE) + + // NOTE: Adding a new value? Remember to update IsValidLoadType! +}; + +static inline bool IsForceReloadType(uint32_t aLoadType) { + switch (aLoadType) { + case LOAD_RELOAD_BYPASS_CACHE: + case LOAD_RELOAD_BYPASS_PROXY: + case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE: + return true; + } + return false; +} + +static inline bool IsValidLoadType(uint32_t aLoadType) { + switch (aLoadType) { + case LOAD_NORMAL: + case LOAD_NORMAL_REPLACE: + case LOAD_NORMAL_BYPASS_CACHE: + case LOAD_NORMAL_BYPASS_PROXY: + case LOAD_NORMAL_BYPASS_PROXY_AND_CACHE: + case LOAD_HISTORY: + case LOAD_RELOAD_NORMAL: + case LOAD_RELOAD_BYPASS_CACHE: + case LOAD_RELOAD_BYPASS_PROXY: + case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE: + case LOAD_LINK: + case LOAD_REFRESH: + case LOAD_REFRESH_REPLACE: + case LOAD_RELOAD_CHARSET_CHANGE: + case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE: + case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE: + case LOAD_BYPASS_HISTORY: + case LOAD_STOP_CONTENT: + case LOAD_STOP_CONTENT_AND_REPLACE: + case LOAD_PUSHSTATE: + case LOAD_REPLACE_BYPASS_CACHE: + case LOAD_ERROR_PAGE: + return true; + } + return false; +} + +inline nsDOMNavigationTiming::Type ConvertLoadTypeToNavigationType( + uint32_t aLoadType) { + // Not initialized, assume it's normal load. + if (aLoadType == 0) { + aLoadType = LOAD_NORMAL; + } + + auto result = nsDOMNavigationTiming::TYPE_RESERVED; + switch (aLoadType) { + case LOAD_NORMAL: + case LOAD_NORMAL_BYPASS_CACHE: + case LOAD_NORMAL_BYPASS_PROXY: + case LOAD_NORMAL_BYPASS_PROXY_AND_CACHE: + case LOAD_NORMAL_REPLACE: + case LOAD_LINK: + case LOAD_STOP_CONTENT: + // FIXME: It isn't clear that LOAD_REFRESH_REPLACE should have a different + // navigation type than LOAD_REFRESH. Those loads historically used the + // LOAD_NORMAL_REPLACE type, and therefore wound up with TYPE_NAVIGATE by + // default. + case LOAD_REFRESH_REPLACE: + case LOAD_REPLACE_BYPASS_CACHE: + result = nsDOMNavigationTiming::TYPE_NAVIGATE; + break; + case LOAD_HISTORY: + result = nsDOMNavigationTiming::TYPE_BACK_FORWARD; + break; + case LOAD_RELOAD_NORMAL: + case LOAD_RELOAD_CHARSET_CHANGE: + case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_PROXY_AND_CACHE: + case LOAD_RELOAD_CHARSET_CHANGE_BYPASS_CACHE: + case LOAD_RELOAD_BYPASS_CACHE: + case LOAD_RELOAD_BYPASS_PROXY: + case LOAD_RELOAD_BYPASS_PROXY_AND_CACHE: + result = nsDOMNavigationTiming::TYPE_RELOAD; + break; + case LOAD_STOP_CONTENT_AND_REPLACE: + case LOAD_REFRESH: + case LOAD_BYPASS_HISTORY: + case LOAD_ERROR_PAGE: + case LOAD_PUSHSTATE: + result = nsDOMNavigationTiming::TYPE_RESERVED; + break; + default: + result = nsDOMNavigationTiming::TYPE_RESERVED; + break; + } + + return result; +} + +#endif // MOZILLA_INTERNAL_API +#endif diff --git a/docshell/base/nsDocShellTelemetryUtils.cpp b/docshell/base/nsDocShellTelemetryUtils.cpp new file mode 100644 index 0000000000..cd78e3bce5 --- /dev/null +++ b/docshell/base/nsDocShellTelemetryUtils.cpp @@ -0,0 +1,206 @@ +/* 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 "nsDocShellTelemetryUtils.h" + +namespace { + +using ErrorLabel = mozilla::Telemetry::LABELS_PAGE_LOAD_ERROR; + +struct LoadErrorTelemetryResult { + nsresult mValue; + ErrorLabel mLabel; +}; + +static const LoadErrorTelemetryResult sResult[] = { + { + NS_ERROR_UNKNOWN_PROTOCOL, + ErrorLabel::UNKNOWN_PROTOCOL, + }, + { + NS_ERROR_FILE_NOT_FOUND, + ErrorLabel::FILE_NOT_FOUND, + }, + { + NS_ERROR_FILE_ACCESS_DENIED, + ErrorLabel::FILE_ACCESS_DENIED, + }, + { + NS_ERROR_UNKNOWN_HOST, + ErrorLabel::UNKNOWN_HOST, + }, + { + NS_ERROR_CONNECTION_REFUSED, + ErrorLabel::CONNECTION_REFUSED, + }, + { + NS_ERROR_PROXY_BAD_GATEWAY, + ErrorLabel::PROXY_BAD_GATEWAY, + }, + { + NS_ERROR_NET_INTERRUPT, + ErrorLabel::NET_INTERRUPT, + }, + { + NS_ERROR_NET_TIMEOUT, + ErrorLabel::NET_TIMEOUT, + }, + { + NS_ERROR_PROXY_GATEWAY_TIMEOUT, + ErrorLabel::P_GATEWAY_TIMEOUT, + }, + { + NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION, + ErrorLabel::CSP_FRAME_ANCEST, + }, + { + NS_ERROR_CSP_FORM_ACTION_VIOLATION, + ErrorLabel::CSP_FORM_ACTION, + }, + { + NS_ERROR_CSP_NAVIGATE_TO_VIOLATION, + ErrorLabel::CSP_NAVIGATE_TO, + }, + { + NS_ERROR_XFO_VIOLATION, + ErrorLabel::XFO_VIOLATION, + }, + { + NS_ERROR_PHISHING_URI, + ErrorLabel::PHISHING_URI, + }, + { + NS_ERROR_MALWARE_URI, + ErrorLabel::MALWARE_URI, + }, + { + NS_ERROR_UNWANTED_URI, + ErrorLabel::UNWANTED_URI, + }, + { + NS_ERROR_HARMFUL_URI, + ErrorLabel::HARMFUL_URI, + }, + { + NS_ERROR_CONTENT_CRASHED, + ErrorLabel::CONTENT_CRASHED, + }, + { + NS_ERROR_FRAME_CRASHED, + ErrorLabel::FRAME_CRASHED, + }, + { + NS_ERROR_BUILDID_MISMATCH, + ErrorLabel::BUILDID_MISMATCH, + }, + { + NS_ERROR_NET_RESET, + ErrorLabel::NET_RESET, + }, + { + NS_ERROR_MALFORMED_URI, + ErrorLabel::MALFORMED_URI, + }, + { + NS_ERROR_REDIRECT_LOOP, + ErrorLabel::REDIRECT_LOOP, + }, + { + NS_ERROR_UNKNOWN_SOCKET_TYPE, + ErrorLabel::UNKNOWN_SOCKET, + }, + { + NS_ERROR_DOCUMENT_NOT_CACHED, + ErrorLabel::DOCUMENT_N_CACHED, + }, + { + NS_ERROR_OFFLINE, + ErrorLabel::OFFLINE, + }, + { + NS_ERROR_DOCUMENT_IS_PRINTMODE, + ErrorLabel::DOC_PRINTMODE, + }, + { + NS_ERROR_PORT_ACCESS_NOT_ALLOWED, + ErrorLabel::PORT_ACCESS, + }, + { + NS_ERROR_UNKNOWN_PROXY_HOST, + ErrorLabel::UNKNOWN_PROXY_HOST, + }, + { + NS_ERROR_PROXY_CONNECTION_REFUSED, + ErrorLabel::PROXY_CONNECTION, + }, + { + NS_ERROR_PROXY_FORBIDDEN, + ErrorLabel::PROXY_FORBIDDEN, + }, + { + NS_ERROR_PROXY_NOT_IMPLEMENTED, + ErrorLabel::P_NOT_IMPLEMENTED, + }, + { + NS_ERROR_PROXY_AUTHENTICATION_FAILED, + ErrorLabel::PROXY_AUTH, + }, + { + NS_ERROR_PROXY_TOO_MANY_REQUESTS, + ErrorLabel::PROXY_TOO_MANY, + }, + { + NS_ERROR_INVALID_CONTENT_ENCODING, + ErrorLabel::CONTENT_ENCODING, + }, + { + NS_ERROR_UNSAFE_CONTENT_TYPE, + ErrorLabel::UNSAFE_CONTENT, + }, + { + NS_ERROR_CORRUPTED_CONTENT, + ErrorLabel::CORRUPTED_CONTENT, + }, + { + NS_ERROR_INTERCEPTION_FAILED, + ErrorLabel::INTERCEPTION_FAIL, + }, + { + NS_ERROR_NET_INADEQUATE_SECURITY, + ErrorLabel::INADEQUATE_SEC, + }, + { + NS_ERROR_BLOCKED_BY_POLICY, + ErrorLabel::BLOCKED_BY_POLICY, + }, + { + NS_ERROR_NET_HTTP2_SENT_GOAWAY, + ErrorLabel::HTTP2_SENT_GOAWAY, + }, + { + NS_ERROR_NET_HTTP3_PROTOCOL_ERROR, + ErrorLabel::HTTP3_PROTOCOL, + }, + { + NS_BINDING_FAILED, + ErrorLabel::BINDING_FAILED, + }, +}; +} // anonymous namespace + +namespace mozilla { +namespace dom { +mozilla::Telemetry::LABELS_PAGE_LOAD_ERROR LoadErrorToTelemetryLabel( + nsresult aRv) { + MOZ_ASSERT(aRv != NS_OK); + + for (const auto& p : sResult) { + if (p.mValue == aRv) { + return p.mLabel; + } + } + return ErrorLabel::otherError; +} +} // namespace dom +} // namespace mozilla diff --git a/docshell/base/nsDocShellTelemetryUtils.h b/docshell/base/nsDocShellTelemetryUtils.h new file mode 100644 index 0000000000..4e0097caec --- /dev/null +++ b/docshell/base/nsDocShellTelemetryUtils.h @@ -0,0 +1,22 @@ +//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsDocShellTelemetryUtils_h__ +#define nsDocShellTelemetryUtils_h__ + +#include "mozilla/Telemetry.h" + +namespace mozilla { +namespace dom { +/** + * Convert page load errors to telemetry labels + * Only select nsresults are converted, otherwise this function + * will return "errorOther", view the list of errors at + * docshell/base/nsDocShellTelemetryUtils.cpp. + */ +Telemetry::LABELS_PAGE_LOAD_ERROR LoadErrorToTelemetryLabel(nsresult aRv); +} // namespace dom +} // namespace mozilla +#endif // nsDocShellTelemetryUtils_h__ diff --git a/docshell/base/nsDocShellTreeOwner.cpp b/docshell/base/nsDocShellTreeOwner.cpp new file mode 100644 index 0000000000..b49dfb6343 --- /dev/null +++ b/docshell/base/nsDocShellTreeOwner.cpp @@ -0,0 +1,1340 @@ +/* -*- 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/. */ + +// Local Includes +#include "nsDocShellTreeOwner.h" +#include "nsWebBrowser.h" + +// Helper Classes +#include "nsContentUtils.h" +#include "nsSize.h" +#include "mozilla/ReflowInput.h" +#include "mozilla/ScopeExit.h" +#include "nsComponentManagerUtils.h" +#include "nsString.h" +#include "nsAtom.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "mozilla/LookAndFeel.h" + +// Interfaces needed to be included +#include "nsPresContext.h" +#include "nsITooltipListener.h" +#include "nsINode.h" +#include "Link.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/SVGTitleElement.h" +#include "nsIFormControl.h" +#include "nsIWebNavigation.h" +#include "nsPIDOMWindow.h" +#include "nsPIWindowRoot.h" +#include "nsIWindowWatcher.h" +#include "nsPIWindowWatcher.h" +#include "nsIPrompt.h" +#include "nsIRemoteTab.h" +#include "nsIBrowserChild.h" +#include "nsRect.h" +#include "nsIWebBrowserChromeFocus.h" +#include "nsIContent.h" +#include "nsServiceManagerUtils.h" +#include "nsViewManager.h" +#include "nsView.h" +#include "nsXULTooltipListener.h" +#include "nsIConstraintValidation.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/dom/DragEvent.h" +#include "mozilla/dom/Event.h" // for Event +#include "mozilla/dom/File.h" // for input type=file +#include "mozilla/dom/FileList.h" // for input type=file +#include "mozilla/dom/LoadURIOptionsBinding.h" +#include "mozilla/PresShell.h" +#include "mozilla/TextEvents.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// A helper routine that navigates the tricky path from a |nsWebBrowser| to +// a |EventTarget| via the window root and chrome event handler. +static nsresult GetDOMEventTarget(nsWebBrowser* aInBrowser, + EventTarget** aTarget) { + if (!aInBrowser) { + return NS_ERROR_INVALID_POINTER; + } + + nsCOMPtr<mozIDOMWindowProxy> domWindow; + aInBrowser->GetContentDOMWindow(getter_AddRefs(domWindow)); + if (!domWindow) { + return NS_ERROR_FAILURE; + } + + auto* outerWindow = nsPIDOMWindowOuter::From(domWindow); + nsPIDOMWindowOuter* rootWindow = outerWindow->GetPrivateRoot(); + NS_ENSURE_TRUE(rootWindow, NS_ERROR_FAILURE); + nsCOMPtr<EventTarget> target = rootWindow->GetChromeEventHandler(); + NS_ENSURE_TRUE(target, NS_ERROR_FAILURE); + target.forget(aTarget); + + return NS_OK; +} + +nsDocShellTreeOwner::nsDocShellTreeOwner() + : mWebBrowser(nullptr), + mTreeOwner(nullptr), + mPrimaryContentShell(nullptr), + mWebBrowserChrome(nullptr), + mOwnerWin(nullptr), + mOwnerRequestor(nullptr) {} + +nsDocShellTreeOwner::~nsDocShellTreeOwner() { RemoveChromeListeners(); } + +NS_IMPL_ADDREF(nsDocShellTreeOwner) +NS_IMPL_RELEASE(nsDocShellTreeOwner) + +NS_INTERFACE_MAP_BEGIN(nsDocShellTreeOwner) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDocShellTreeOwner) + NS_INTERFACE_MAP_ENTRY(nsIDocShellTreeOwner) + NS_INTERFACE_MAP_ENTRY(nsIBaseWindow) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +// The class that listens to the chrome events and tells the embedding chrome to +// show tooltips, as appropriate. Handles registering itself with the DOM with +// AddChromeListeners() and removing itself with RemoveChromeListeners(). +class ChromeTooltipListener final : public nsIDOMEventListener { + protected: + virtual ~ChromeTooltipListener(); + + public: + NS_DECL_ISUPPORTS + + ChromeTooltipListener(nsWebBrowser* aInBrowser, + nsIWebBrowserChrome* aInChrome); + + NS_DECL_NSIDOMEVENTLISTENER + NS_IMETHOD MouseMove(mozilla::dom::Event* aMouseEvent); + + // Add/remove the relevant listeners, based on what interfaces the embedding + // chrome implements. + NS_IMETHOD AddChromeListeners(); + NS_IMETHOD RemoveChromeListeners(); + + NS_IMETHOD HideTooltip(); + + bool WebProgressShowedTooltip(nsIWebProgress* aWebProgress); + + private: + // pixel tolerance for mousemove event + static constexpr CSSIntCoord kTooltipMouseMoveTolerance = 7; + + NS_IMETHOD AddTooltipListener(); + NS_IMETHOD RemoveTooltipListener(); + + NS_IMETHOD ShowTooltip(int32_t aInXCoords, int32_t aInYCoords, + const nsAString& aInTipText, + const nsAString& aDirText); + nsITooltipTextProvider* GetTooltipTextProvider(); + + nsWebBrowser* mWebBrowser; + nsCOMPtr<mozilla::dom::EventTarget> mEventTarget; + nsCOMPtr<nsITooltipTextProvider> mTooltipTextProvider; + + // This must be a strong ref in order to make sure we can hide the tooltip if + // the window goes away while we're displaying one. If we don't hold a strong + // ref, the chrome might have been disposed of before we get a chance to tell + // it, and no one would ever tell us of that fact. + nsCOMPtr<nsIWebBrowserChrome> mWebBrowserChrome; + + bool mTooltipListenerInstalled; + + nsCOMPtr<nsITimer> mTooltipTimer; + static void sTooltipCallback(nsITimer* aTimer, void* aListener); + + // Mouse coordinates for last mousemove event we saw + CSSIntPoint mMouseClientPoint; + + // Mouse coordinates for tooltip event + LayoutDeviceIntPoint mMouseScreenPoint; + + bool mShowingTooltip; + + bool mTooltipShownOnce; + + // The string of text that we last displayed. + nsString mLastShownTooltipText; + + nsWeakPtr mLastDocshell; + + // The node hovered over that fired the timer. This may turn into the node + // that triggered the tooltip, but only if the timer ever gets around to + // firing. This is a strong reference, because the tooltip content can be + // destroyed while we're waiting for the tooltip to pop up, and we need to + // detect that. It's set only when the tooltip timer is created and launched. + // The timer must either fire or be cancelled (or possibly released?), and we + // release this reference in each of those cases. So we don't leak. + nsCOMPtr<nsINode> mPossibleTooltipNode; +}; + +//***************************************************************************** +// nsDocShellTreeOwner::nsIInterfaceRequestor +//***************************************************************************** + +NS_IMETHODIMP +nsDocShellTreeOwner::GetInterface(const nsIID& aIID, void** aSink) { + NS_ENSURE_ARG_POINTER(aSink); + + if (NS_SUCCEEDED(QueryInterface(aIID, aSink))) { + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsIWebBrowserChromeFocus))) { + if (mWebBrowserChromeWeak != nullptr) { + return mWebBrowserChromeWeak->QueryReferent(aIID, aSink); + } + return mOwnerWin->QueryInterface(aIID, aSink); + } + + if (aIID.Equals(NS_GET_IID(nsIPrompt))) { + nsCOMPtr<nsIPrompt> prompt; + EnsurePrompter(); + prompt = mPrompter; + if (prompt) { + prompt.forget(aSink); + return NS_OK; + } + return NS_NOINTERFACE; + } + + if (aIID.Equals(NS_GET_IID(nsIAuthPrompt))) { + nsCOMPtr<nsIAuthPrompt> prompt; + EnsureAuthPrompter(); + prompt = mAuthPrompter; + if (prompt) { + prompt.forget(aSink); + return NS_OK; + } + return NS_NOINTERFACE; + } + + nsCOMPtr<nsIInterfaceRequestor> req = GetOwnerRequestor(); + if (req) { + return req->GetInterface(aIID, aSink); + } + + return NS_NOINTERFACE; +} + +//***************************************************************************** +// nsDocShellTreeOwner::nsIDocShellTreeOwner +//***************************************************************************** + +void nsDocShellTreeOwner::EnsurePrompter() { + if (mPrompter) { + return; + } + + nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + if (wwatch && mWebBrowser) { + nsCOMPtr<mozIDOMWindowProxy> domWindow; + mWebBrowser->GetContentDOMWindow(getter_AddRefs(domWindow)); + if (domWindow) { + wwatch->GetNewPrompter(domWindow, getter_AddRefs(mPrompter)); + } + } +} + +void nsDocShellTreeOwner::EnsureAuthPrompter() { + if (mAuthPrompter) { + return; + } + + nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + if (wwatch && mWebBrowser) { + nsCOMPtr<mozIDOMWindowProxy> domWindow; + mWebBrowser->GetContentDOMWindow(getter_AddRefs(domWindow)); + if (domWindow) { + wwatch->GetNewAuthPrompter(domWindow, getter_AddRefs(mAuthPrompter)); + } + } +} + +void nsDocShellTreeOwner::AddToWatcher() { + if (mWebBrowser) { + nsCOMPtr<mozIDOMWindowProxy> domWindow; + mWebBrowser->GetContentDOMWindow(getter_AddRefs(domWindow)); + if (domWindow) { + nsCOMPtr<nsPIWindowWatcher> wwatch( + do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + if (wwatch) { + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome(); + if (webBrowserChrome) { + wwatch->AddWindow(domWindow, webBrowserChrome); + } + } + } + } +} + +void nsDocShellTreeOwner::RemoveFromWatcher() { + if (mWebBrowser) { + nsCOMPtr<mozIDOMWindowProxy> domWindow; + mWebBrowser->GetContentDOMWindow(getter_AddRefs(domWindow)); + if (domWindow) { + nsCOMPtr<nsPIWindowWatcher> wwatch( + do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + if (wwatch) { + wwatch->RemoveWindow(domWindow); + } + } + } +} + +void nsDocShellTreeOwner::EnsureContentTreeOwner() { + if (mContentTreeOwner) { + return; + } + + mContentTreeOwner = new nsDocShellTreeOwner(); + nsCOMPtr<nsIWebBrowserChrome> browserChrome = GetWebBrowserChrome(); + if (browserChrome) { + mContentTreeOwner->SetWebBrowserChrome(browserChrome); + } + + if (mWebBrowser) { + mContentTreeOwner->WebBrowser(mWebBrowser); + } +} + +NS_IMETHODIMP +nsDocShellTreeOwner::ContentShellAdded(nsIDocShellTreeItem* aContentShell, + bool aPrimary) { + if (mTreeOwner) return mTreeOwner->ContentShellAdded(aContentShell, aPrimary); + + EnsureContentTreeOwner(); + aContentShell->SetTreeOwner(mContentTreeOwner); + + if (aPrimary) { + mPrimaryContentShell = aContentShell; + mPrimaryRemoteTab = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::ContentShellRemoved(nsIDocShellTreeItem* aContentShell) { + if (mTreeOwner) { + return mTreeOwner->ContentShellRemoved(aContentShell); + } + + if (mPrimaryContentShell == aContentShell) { + mPrimaryContentShell = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetPrimaryContentShell(nsIDocShellTreeItem** aShell) { + NS_ENSURE_ARG_POINTER(aShell); + + if (mTreeOwner) { + return mTreeOwner->GetPrimaryContentShell(aShell); + } + + nsCOMPtr<nsIDocShellTreeItem> shell; + if (!mPrimaryRemoteTab) { + shell = + mPrimaryContentShell ? mPrimaryContentShell : mWebBrowser->mDocShell; + } + shell.forget(aShell); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::RemoteTabAdded(nsIRemoteTab* aTab, bool aPrimary) { + if (mTreeOwner) { + return mTreeOwner->RemoteTabAdded(aTab, aPrimary); + } + + if (aPrimary) { + mPrimaryRemoteTab = aTab; + mPrimaryContentShell = nullptr; + } else if (mPrimaryRemoteTab == aTab) { + mPrimaryRemoteTab = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::RemoteTabRemoved(nsIRemoteTab* aTab) { + if (mTreeOwner) { + return mTreeOwner->RemoteTabRemoved(aTab); + } + + if (aTab == mPrimaryRemoteTab) { + mPrimaryRemoteTab = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetPrimaryRemoteTab(nsIRemoteTab** aTab) { + if (mTreeOwner) { + return mTreeOwner->GetPrimaryRemoteTab(aTab); + } + + nsCOMPtr<nsIRemoteTab> tab = mPrimaryRemoteTab; + tab.forget(aTab); + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetPrimaryContentBrowsingContext( + mozilla::dom::BrowsingContext** aBc) { + if (mTreeOwner) { + return mTreeOwner->GetPrimaryContentBrowsingContext(aBc); + } + if (mPrimaryRemoteTab) { + return mPrimaryRemoteTab->GetBrowsingContext(aBc); + } + if (mPrimaryContentShell) { + return mPrimaryContentShell->GetBrowsingContextXPCOM(aBc); + } + if (mWebBrowser->mDocShell) { + return mWebBrowser->mDocShell->GetBrowsingContextXPCOM(aBc); + } + *aBc = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetPrimaryContentSize(int32_t* aWidth, int32_t* aHeight) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetPrimaryContentSize(int32_t aWidth, int32_t aHeight) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetRootShellSize(int32_t* aWidth, int32_t* aHeight) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetRootShellSize(int32_t aWidth, int32_t aHeight) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SizeShellTo(nsIDocShellTreeItem* aShellItem, int32_t aCX, + int32_t aCY) { + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome(); + + NS_ENSURE_STATE(mTreeOwner || webBrowserChrome); + + if (nsCOMPtr<nsIDocShellTreeOwner> treeOwner = mTreeOwner) { + return treeOwner->SizeShellTo(aShellItem, aCX, aCY); + } + + if (aShellItem == mWebBrowser->mDocShell) { + nsCOMPtr<nsIBrowserChild> browserChild = + do_QueryInterface(webBrowserChrome); + if (browserChild) { + // The XUL window to resize is in the parent process, but there we + // won't be able to get the size of aShellItem. We can ask the parent + // process to change our size instead. + nsCOMPtr<nsIBaseWindow> shellAsWin(do_QueryInterface(aShellItem)); + NS_ENSURE_TRUE(shellAsWin, NS_ERROR_FAILURE); + + LayoutDeviceIntSize shellSize; + shellAsWin->GetSize(&shellSize.width, &shellSize.height); + LayoutDeviceIntSize deltaSize = LayoutDeviceIntSize(aCX, aCY) - shellSize; + + LayoutDeviceIntSize currentSize; + GetSize(¤tSize.width, ¤tSize.height); + + LayoutDeviceIntSize newSize = currentSize + deltaSize; + return SetSize(newSize.width, newSize.height, true); + } + // XXX: this is weird, but we used to call a method here + // (webBrowserChrome->SizeBrowserTo()) whose implementations all failed + // like this, so... + return NS_ERROR_NOT_IMPLEMENTED; + } + + MOZ_ASSERT_UNREACHABLE("This is unimplemented, API should be cleaned up"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetPersistence(bool aPersistPosition, bool aPersistSize, + bool aPersistSizeMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetPersistence(bool* aPersistPosition, bool* aPersistSize, + bool* aPersistSizeMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetTabCount(uint32_t* aResult) { + if (mTreeOwner) { + return mTreeOwner->GetTabCount(aResult); + } + + *aResult = 0; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetHasPrimaryContent(bool* aResult) { + *aResult = mPrimaryRemoteTab || mPrimaryContentShell; + return NS_OK; +} + +//***************************************************************************** +// nsDocShellTreeOwner::nsIBaseWindow +//***************************************************************************** + +NS_IMETHODIMP +nsDocShellTreeOwner::InitWindow(nativeWindow aParentNativeWindow, + nsIWidget* aParentWidget, int32_t aX, + int32_t aY, int32_t aCX, int32_t aCY) { + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::Destroy() { + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome(); + if (webBrowserChrome) { + // XXX: this is weird, but we used to call a method here + // (webBrowserChrome->DestroyBrowserWindow()) whose implementations all + // failed like this, so... + return NS_ERROR_NOT_IMPLEMENTED; + } + + return NS_ERROR_NULL_POINTER; +} + +double nsDocShellTreeOwner::GetWidgetCSSToDeviceScale() { + return mWebBrowser ? mWebBrowser->GetWidgetCSSToDeviceScale() : 1.0; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetDevicePixelsPerDesktopPixel(double* aScale) { + if (mWebBrowser) { + return mWebBrowser->GetDevicePixelsPerDesktopPixel(aScale); + } + + *aScale = 1.0; + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetPositionDesktopPix(int32_t aX, int32_t aY) { + if (mWebBrowser) { + nsresult rv = mWebBrowser->SetPositionDesktopPix(aX, aY); + NS_ENSURE_SUCCESS(rv, rv); + } + + double scale = 1.0; + GetDevicePixelsPerDesktopPixel(&scale); + return SetPosition(NSToIntRound(aX * scale), NSToIntRound(aY * scale)); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetPosition(int32_t aX, int32_t aY) { + return SetDimensions( + {DimensionKind::Outer, Some(aX), Some(aY), Nothing(), Nothing()}); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetPosition(int32_t* aX, int32_t* aY) { + return GetDimensions(DimensionKind::Outer, aX, aY, nullptr, nullptr); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetSize(int32_t aCX, int32_t aCY, bool aRepaint) { + return SetDimensions( + {DimensionKind::Outer, Nothing(), Nothing(), Some(aCX), Some(aCY)}); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetSize(int32_t* aCX, int32_t* aCY) { + return GetDimensions(DimensionKind::Outer, nullptr, nullptr, aCX, aCY); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetPositionAndSize(int32_t aX, int32_t aY, int32_t aCX, + int32_t aCY, uint32_t aFlags) { + return SetDimensions( + {DimensionKind::Outer, Some(aX), Some(aY), Some(aCX), Some(aCY)}); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetPositionAndSize(int32_t* aX, int32_t* aY, int32_t* aCX, + int32_t* aCY) { + return GetDimensions(DimensionKind::Outer, aX, aY, aCX, aCY); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetDimensions(DimensionRequest&& aRequest) { + nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin(); + if (ownerWin) { + return ownerWin->SetDimensions(std::move(aRequest)); + } + + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome(); + NS_ENSURE_STATE(webBrowserChrome); + return webBrowserChrome->SetDimensions(std::move(aRequest)); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetDimensions(DimensionKind aDimensionKind, int32_t* aX, + int32_t* aY, int32_t* aCX, int32_t* aCY) { + nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin(); + if (ownerWin) { + return ownerWin->GetDimensions(aDimensionKind, aX, aY, aCX, aCY); + } + + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome(); + NS_ENSURE_STATE(webBrowserChrome); + return webBrowserChrome->GetDimensions(aDimensionKind, aX, aY, aCX, aCY); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::Repaint(bool aForce) { return NS_ERROR_NULL_POINTER; } + +NS_IMETHODIMP +nsDocShellTreeOwner::GetParentWidget(nsIWidget** aParentWidget) { + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetParentWidget(nsIWidget* aParentWidget) { + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetParentNativeWindow(nativeWindow* aParentNativeWindow) { + nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin(); + if (ownerWin) { + return ownerWin->GetParentNativeWindow(aParentNativeWindow); + } + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetParentNativeWindow(nativeWindow aParentNativeWindow) { + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetNativeHandle(nsAString& aNativeHandle) { + // the nativeHandle should be accessed from nsIAppWindow + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetVisibility(bool* aVisibility) { + nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin(); + if (ownerWin) { + return ownerWin->GetVisibility(aVisibility); + } + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetVisibility(bool aVisibility) { + nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin(); + if (ownerWin) { + return ownerWin->SetVisibility(aVisibility); + } + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetEnabled(bool* aEnabled) { + NS_ENSURE_ARG_POINTER(aEnabled); + *aEnabled = true; + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetEnabled(bool aEnabled) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetMainWidget(nsIWidget** aMainWidget) { + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::GetTitle(nsAString& aTitle) { + nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin(); + if (ownerWin) { + return ownerWin->GetTitle(aTitle); + } + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetTitle(const nsAString& aTitle) { + nsCOMPtr<nsIBaseWindow> ownerWin = GetOwnerWin(); + if (ownerWin) { + return ownerWin->SetTitle(aTitle); + } + return NS_ERROR_NULL_POINTER; +} + +//***************************************************************************** +// nsDocShellTreeOwner::nsIWebProgressListener +//***************************************************************************** + +NS_IMETHODIMP +nsDocShellTreeOwner::OnProgressChange(nsIWebProgress* aProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + // In the absence of DOM document creation event, this method is the + // most convenient place to install the mouse listener on the + // DOM document. + return AddChromeListeners(); +} + +NS_IMETHODIMP +nsDocShellTreeOwner::OnStateChange(nsIWebProgress* aProgress, + nsIRequest* aRequest, + uint32_t aProgressStateFlags, + nsresult aStatus) { + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aURI, + uint32_t aFlags) { + if (mChromeTooltipListener && aWebProgress && + !(aFlags & nsIWebProgressListener::LOCATION_CHANGE_SAME_DOCUMENT) && + mChromeTooltipListener->WebProgressShowedTooltip(aWebProgress)) { + mChromeTooltipListener->HideTooltip(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) { + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + return NS_OK; +} + +//***************************************************************************** +// nsDocShellTreeOwner: Accessors +//***************************************************************************** + +void nsDocShellTreeOwner::WebBrowser(nsWebBrowser* aWebBrowser) { + if (!aWebBrowser) { + RemoveChromeListeners(); + } + if (aWebBrowser != mWebBrowser) { + mPrompter = nullptr; + mAuthPrompter = nullptr; + } + + mWebBrowser = aWebBrowser; + + if (mContentTreeOwner) { + mContentTreeOwner->WebBrowser(aWebBrowser); + if (!aWebBrowser) { + mContentTreeOwner = nullptr; + } + } +} + +nsWebBrowser* nsDocShellTreeOwner::WebBrowser() { return mWebBrowser; } + +NS_IMETHODIMP +nsDocShellTreeOwner::SetTreeOwner(nsIDocShellTreeOwner* aTreeOwner) { + if (aTreeOwner) { + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome(do_GetInterface(aTreeOwner)); + NS_ENSURE_TRUE(webBrowserChrome, NS_ERROR_INVALID_ARG); + NS_ENSURE_SUCCESS(SetWebBrowserChrome(webBrowserChrome), + NS_ERROR_INVALID_ARG); + mTreeOwner = aTreeOwner; + } else { + mTreeOwner = nullptr; + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome(); + if (!webBrowserChrome) { + NS_ENSURE_SUCCESS(SetWebBrowserChrome(nullptr), NS_ERROR_FAILURE); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::SetWebBrowserChrome( + nsIWebBrowserChrome* aWebBrowserChrome) { + if (!aWebBrowserChrome) { + mWebBrowserChrome = nullptr; + mOwnerWin = nullptr; + mOwnerRequestor = nullptr; + mWebBrowserChromeWeak = nullptr; + } else { + nsCOMPtr<nsISupportsWeakReference> supportsweak = + do_QueryInterface(aWebBrowserChrome); + if (supportsweak) { + supportsweak->GetWeakReference(getter_AddRefs(mWebBrowserChromeWeak)); + } else { + nsCOMPtr<nsIBaseWindow> ownerWin(do_QueryInterface(aWebBrowserChrome)); + nsCOMPtr<nsIInterfaceRequestor> requestor( + do_QueryInterface(aWebBrowserChrome)); + + // it's ok for ownerWin or requestor to be null. + mWebBrowserChrome = aWebBrowserChrome; + mOwnerWin = ownerWin; + mOwnerRequestor = requestor; + } + } + + if (mContentTreeOwner) { + mContentTreeOwner->SetWebBrowserChrome(aWebBrowserChrome); + } + + return NS_OK; +} + +// Hook up things to the chrome like context menus and tooltips, if the chrome +// has implemented the right interfaces. +NS_IMETHODIMP +nsDocShellTreeOwner::AddChromeListeners() { + nsresult rv = NS_OK; + + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome(); + if (!webBrowserChrome) { + return NS_ERROR_FAILURE; + } + + // install tooltips + if (!mChromeTooltipListener) { + nsCOMPtr<nsITooltipListener> tooltipListener( + do_QueryInterface(webBrowserChrome)); + if (tooltipListener) { + mChromeTooltipListener = + new ChromeTooltipListener(mWebBrowser, webBrowserChrome); + rv = mChromeTooltipListener->AddChromeListeners(); + } + } + + nsCOMPtr<EventTarget> target; + GetDOMEventTarget(mWebBrowser, getter_AddRefs(target)); + + // register dragover and drop event listeners with the listener manager + MOZ_ASSERT(target, "how does this happen? (see bug 1659758)"); + if (target) { + if (EventListenerManager* elmP = target->GetOrCreateListenerManager()) { + elmP->AddEventListenerByType(this, u"dragover"_ns, + TrustedEventsAtSystemGroupBubble()); + elmP->AddEventListenerByType(this, u"drop"_ns, + TrustedEventsAtSystemGroupBubble()); + } + } + + return rv; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::RemoveChromeListeners() { + if (mChromeTooltipListener) { + mChromeTooltipListener->RemoveChromeListeners(); + mChromeTooltipListener = nullptr; + } + + nsCOMPtr<EventTarget> piTarget; + GetDOMEventTarget(mWebBrowser, getter_AddRefs(piTarget)); + if (!piTarget) { + return NS_OK; + } + + EventListenerManager* elmP = piTarget->GetOrCreateListenerManager(); + if (elmP) { + elmP->RemoveEventListenerByType(this, u"dragover"_ns, + TrustedEventsAtSystemGroupBubble()); + elmP->RemoveEventListenerByType(this, u"drop"_ns, + TrustedEventsAtSystemGroupBubble()); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShellTreeOwner::HandleEvent(Event* aEvent) { + DragEvent* dragEvent = aEvent ? aEvent->AsDragEvent() : nullptr; + if (NS_WARN_IF(!dragEvent)) { + return NS_ERROR_INVALID_ARG; + } + + if (dragEvent->DefaultPrevented()) { + return NS_OK; + } + + nsCOMPtr<nsIDroppedLinkHandler> handler = + do_GetService("@mozilla.org/content/dropped-link-handler;1"); + if (!handler) { + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("dragover")) { + bool canDropLink = false; + handler->CanDropLink(dragEvent, false, &canDropLink); + if (canDropLink) { + aEvent->PreventDefault(); + } + } else if (eventType.EqualsLiteral("drop")) { + nsCOMPtr<nsIWebNavigation> webnav = + static_cast<nsIWebNavigation*>(mWebBrowser); + + // The page might have cancelled the dragover event itself, so check to + // make sure that the link can be dropped first. + bool canDropLink = false; + handler->CanDropLink(dragEvent, false, &canDropLink); + if (!canDropLink) { + return NS_OK; + } + + nsTArray<RefPtr<nsIDroppedLinkItem>> links; + if (webnav && NS_SUCCEEDED(handler->DropLinks(dragEvent, true, links))) { + if (links.Length() >= 1) { + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + handler->GetTriggeringPrincipal(dragEvent, + getter_AddRefs(triggeringPrincipal)); + if (triggeringPrincipal) { + nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = + GetWebBrowserChrome(); + if (webBrowserChrome) { + nsCOMPtr<nsIBrowserChild> browserChild = + do_QueryInterface(webBrowserChrome); + if (browserChild) { + nsresult rv = browserChild->RemoteDropLinks(links); + return rv; + } + } + nsAutoString url; + if (NS_SUCCEEDED(links[0]->GetUrl(url))) { + if (!url.IsEmpty()) { +#ifndef ANDROID + MOZ_ASSERT(triggeringPrincipal, + "nsDocShellTreeOwner::HandleEvent: Need a valid " + "triggeringPrincipal"); +#endif + LoadURIOptions loadURIOptions; + loadURIOptions.mTriggeringPrincipal = triggeringPrincipal; + nsCOMPtr<nsIContentSecurityPolicy> csp; + handler->GetCsp(dragEvent, getter_AddRefs(csp)); + loadURIOptions.mCsp = csp; + webnav->FixupAndLoadURIString(url, loadURIOptions); + } + } + } + } + } else { + aEvent->StopPropagation(); + aEvent->PreventDefault(); + } + } + + return NS_OK; +} + +already_AddRefed<nsIWebBrowserChrome> +nsDocShellTreeOwner::GetWebBrowserChrome() { + nsCOMPtr<nsIWebBrowserChrome> chrome; + if (mWebBrowserChromeWeak) { + chrome = do_QueryReferent(mWebBrowserChromeWeak); + } else if (mWebBrowserChrome) { + chrome = mWebBrowserChrome; + } + return chrome.forget(); +} + +already_AddRefed<nsIBaseWindow> nsDocShellTreeOwner::GetOwnerWin() { + nsCOMPtr<nsIBaseWindow> win; + if (mWebBrowserChromeWeak) { + win = do_QueryReferent(mWebBrowserChromeWeak); + } else if (mOwnerWin) { + win = mOwnerWin; + } + return win.forget(); +} + +already_AddRefed<nsIInterfaceRequestor> +nsDocShellTreeOwner::GetOwnerRequestor() { + nsCOMPtr<nsIInterfaceRequestor> req; + if (mWebBrowserChromeWeak) { + req = do_QueryReferent(mWebBrowserChromeWeak); + } else if (mOwnerRequestor) { + req = mOwnerRequestor; + } + return req.forget(); +} + +NS_IMPL_ISUPPORTS(ChromeTooltipListener, nsIDOMEventListener) + +ChromeTooltipListener::ChromeTooltipListener(nsWebBrowser* aInBrowser, + nsIWebBrowserChrome* aInChrome) + : mWebBrowser(aInBrowser), + mWebBrowserChrome(aInChrome), + mTooltipListenerInstalled(false), + mShowingTooltip(false), + mTooltipShownOnce(false) {} + +ChromeTooltipListener::~ChromeTooltipListener() {} + +nsITooltipTextProvider* ChromeTooltipListener::GetTooltipTextProvider() { + if (!mTooltipTextProvider) { + mTooltipTextProvider = do_GetService(NS_TOOLTIPTEXTPROVIDER_CONTRACTID); + } + + if (!mTooltipTextProvider) { + mTooltipTextProvider = + do_GetService(NS_DEFAULTTOOLTIPTEXTPROVIDER_CONTRACTID); + } + + return mTooltipTextProvider; +} + +// Hook up things to the chrome like context menus and tooltips, if the chrome +// has implemented the right interfaces. +NS_IMETHODIMP +ChromeTooltipListener::AddChromeListeners() { + if (!mEventTarget) { + GetDOMEventTarget(mWebBrowser, getter_AddRefs(mEventTarget)); + } + + // Register the appropriate events for tooltips, but only if + // the embedding chrome cares. + nsresult rv = NS_OK; + nsCOMPtr<nsITooltipListener> tooltipListener( + do_QueryInterface(mWebBrowserChrome)); + if (tooltipListener && !mTooltipListenerInstalled) { + rv = AddTooltipListener(); + if (NS_FAILED(rv)) { + return rv; + } + } + + return rv; +} + +// Subscribe to the events that will allow us to track tooltips. We need "mouse" +// for mouseExit, "mouse motion" for mouseMove, and "key" for keyDown. As we +// add the listeners, keep track of how many succeed so we can clean up +// correctly in Release(). +NS_IMETHODIMP +ChromeTooltipListener::AddTooltipListener() { + if (mEventTarget) { + MOZ_TRY(mEventTarget->AddSystemEventListener(u"keydown"_ns, this, false, + false)); + MOZ_TRY(mEventTarget->AddSystemEventListener(u"mousedown"_ns, this, false, + false)); + MOZ_TRY(mEventTarget->AddSystemEventListener(u"mouseout"_ns, this, false, + false)); + MOZ_TRY(mEventTarget->AddSystemEventListener(u"mousemove"_ns, this, false, + false)); + + mTooltipListenerInstalled = true; + } + + return NS_OK; +} + +// Unsubscribe from the various things we've hooked up to the window root. +NS_IMETHODIMP +ChromeTooltipListener::RemoveChromeListeners() { + HideTooltip(); + + if (mTooltipListenerInstalled) { + RemoveTooltipListener(); + } + + mEventTarget = nullptr; + + // it really doesn't matter if these fail... + return NS_OK; +} + +// Unsubscribe from all the various tooltip events that we were listening to. +NS_IMETHODIMP +ChromeTooltipListener::RemoveTooltipListener() { + if (mEventTarget) { + mEventTarget->RemoveSystemEventListener(u"keydown"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"mousedown"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"mouseout"_ns, this, false); + mEventTarget->RemoveSystemEventListener(u"mousemove"_ns, this, false); + mTooltipListenerInstalled = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +ChromeTooltipListener::HandleEvent(Event* aEvent) { + nsAutoString eventType; + aEvent->GetType(eventType); + + if (eventType.EqualsLiteral("mousedown")) { + return HideTooltip(); + } else if (eventType.EqualsLiteral("keydown")) { + WidgetKeyboardEvent* keyEvent = aEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (nsXULTooltipListener::KeyEventHidesTooltip(*keyEvent)) { + return HideTooltip(); + } + return NS_OK; + } else if (eventType.EqualsLiteral("mouseout")) { + // Reset flag so that tooltip will display on the next MouseMove + mTooltipShownOnce = false; + return HideTooltip(); + } else if (eventType.EqualsLiteral("mousemove")) { + return MouseMove(aEvent); + } + + NS_ERROR("Unexpected event type"); + return NS_OK; +} + +// If we're a tooltip, fire off a timer to see if a tooltip should be shown. If +// the timer fires, we cache the node in |mPossibleTooltipNode|. +nsresult ChromeTooltipListener::MouseMove(Event* aMouseEvent) { + if (!nsXULTooltipListener::ShowTooltips()) { + return NS_OK; + } + + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + if (!mouseEvent) { + return NS_OK; + } + + // stash the coordinates of the event so that we can still get back to it from + // within the timer callback. On win32, we'll get a MouseMove event even when + // a popup goes away -- even when the mouse doesn't change position! To get + // around this, we make sure the mouse has really moved before proceeding. + CSSIntPoint newMouseClientPoint = mouseEvent->ClientPoint(); + if (mMouseClientPoint == newMouseClientPoint) { + return NS_OK; + } + + // Filter out minor mouse movements. + if (mShowingTooltip && + (abs(mMouseClientPoint.x - newMouseClientPoint.x) <= + kTooltipMouseMoveTolerance) && + (abs(mMouseClientPoint.y - newMouseClientPoint.y) <= + kTooltipMouseMoveTolerance)) { + return NS_OK; + } + + mMouseClientPoint = newMouseClientPoint; + mMouseScreenPoint = mouseEvent->ScreenPointLayoutDevicePix(); + + if (mTooltipTimer) { + mTooltipTimer->Cancel(); + mTooltipTimer = nullptr; + } + + if (!mShowingTooltip) { + nsIEventTarget* target = nullptr; + if (nsCOMPtr<EventTarget> eventTarget = aMouseEvent->GetComposedTarget()) { + mPossibleTooltipNode = nsINode::FromEventTarget(eventTarget); + nsCOMPtr<nsIGlobalObject> global(eventTarget->GetOwnerGlobal()); + if (global) { + target = global->EventTargetFor(TaskCategory::UI); + } + } + + if (mPossibleTooltipNode) { + nsresult rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(mTooltipTimer), sTooltipCallback, this, + LookAndFeel::GetInt(LookAndFeel::IntID::TooltipDelay, 500), + nsITimer::TYPE_ONE_SHOT, "ChromeTooltipListener::MouseMove", target); + if (NS_FAILED(rv)) { + mPossibleTooltipNode = nullptr; + NS_WARNING("Could not create a timer for tooltip tracking"); + } + } + } else { + mTooltipShownOnce = true; + return HideTooltip(); + } + + return NS_OK; +} + +// Tell the registered chrome that they should show the tooltip. +NS_IMETHODIMP +ChromeTooltipListener::ShowTooltip(int32_t aInXCoords, int32_t aInYCoords, + const nsAString& aInTipText, + const nsAString& aTipDir) { + nsresult rv = NS_OK; + + // do the work to call the client + nsCOMPtr<nsITooltipListener> tooltipListener( + do_QueryInterface(mWebBrowserChrome)); + if (tooltipListener) { + rv = tooltipListener->OnShowTooltip(aInXCoords, aInYCoords, aInTipText, + aTipDir); + if (NS_SUCCEEDED(rv)) { + mShowingTooltip = true; + } + } + + return rv; +} + +// Tell the registered chrome that they should rollup the tooltip +// NOTE: This routine is safe to call even if the popup is already closed. +NS_IMETHODIMP +ChromeTooltipListener::HideTooltip() { + nsresult rv = NS_OK; + + // shut down the relevant timers + if (mTooltipTimer) { + mTooltipTimer->Cancel(); + mTooltipTimer = nullptr; + // release tooltip target + mPossibleTooltipNode = nullptr; + mLastDocshell = nullptr; + } + + // if we're showing the tip, tell the chrome to hide it + if (mShowingTooltip) { + nsCOMPtr<nsITooltipListener> tooltipListener( + do_QueryInterface(mWebBrowserChrome)); + if (tooltipListener) { + rv = tooltipListener->OnHideTooltip(); + if (NS_SUCCEEDED(rv)) { + mShowingTooltip = false; + } + } + } + + return rv; +} + +bool ChromeTooltipListener::WebProgressShowedTooltip( + nsIWebProgress* aWebProgress) { + nsCOMPtr<nsIDocShell> docshell = do_QueryInterface(aWebProgress); + nsCOMPtr<nsIDocShell> lastUsed = do_QueryReferent(mLastDocshell); + while (lastUsed) { + if (lastUsed == docshell) { + return true; + } + // We can't use the docshell hierarchy here, because when the parent + // docshell is navigated, the child docshell is disconnected (ie its + // references to the parent are nulled out) despite it still being + // alive here. So we use the document hierarchy instead: + Document* document = lastUsed->GetDocument(); + if (document) { + document = document->GetInProcessParentDocument(); + } + if (!document) { + break; + } + lastUsed = document->GetDocShell(); + } + return false; +} + +// A timer callback, fired when the mouse has hovered inside of a frame for the +// appropriate amount of time. Getting to this point means that we should show +// the tooltip, but only after we determine there is an appropriate TITLE +// element. +// +// This relies on certain things being cached into the |aChromeTooltipListener| +// object passed to us by the timer: +// -- the x/y coordinates of the mouse (mMouseClientY, mMouseClientX) +// -- the dom node the user hovered over (mPossibleTooltipNode) +void ChromeTooltipListener::sTooltipCallback(nsITimer* aTimer, + void* aChromeTooltipListener) { + auto* self = static_cast<ChromeTooltipListener*>(aChromeTooltipListener); + if (!self || !self->mPossibleTooltipNode) { + return; + } + // release tooltip target once done, no matter what we do here. + auto cleanup = MakeScopeExit([&] { self->mPossibleTooltipNode = nullptr; }); + if (!self->mPossibleTooltipNode->IsInComposedDoc()) { + return; + } + // Check that the document or its ancestors haven't been replaced. + { + Document* doc = self->mPossibleTooltipNode->OwnerDoc(); + while (doc) { + if (!doc->IsCurrentActiveDocument()) { + return; + } + doc = doc->GetInProcessParentDocument(); + } + } + + nsCOMPtr<nsIDocShell> docShell = + do_GetInterface(static_cast<nsIWebBrowser*>(self->mWebBrowser)); + if (!docShell || !docShell->GetBrowsingContext()->IsActive()) { + return; + } + + // if there is text associated with the node, show the tip and fire + // off a timer to auto-hide it. + nsITooltipTextProvider* tooltipProvider = self->GetTooltipTextProvider(); + if (!tooltipProvider) { + return; + } + nsString tooltipText; + nsString directionText; + bool textFound = false; + tooltipProvider->GetNodeText(self->mPossibleTooltipNode, + getter_Copies(tooltipText), + getter_Copies(directionText), &textFound); + + if (textFound && (!self->mTooltipShownOnce || + tooltipText != self->mLastShownTooltipText)) { + // ShowTooltip expects screen-relative position. + self->ShowTooltip(self->mMouseScreenPoint.x, self->mMouseScreenPoint.y, + tooltipText, directionText); + self->mLastShownTooltipText = std::move(tooltipText); + self->mLastDocshell = do_GetWeakReference( + self->mPossibleTooltipNode->OwnerDoc()->GetDocShell()); + } +} diff --git a/docshell/base/nsDocShellTreeOwner.h b/docshell/base/nsDocShellTreeOwner.h new file mode 100644 index 0000000000..0627357606 --- /dev/null +++ b/docshell/base/nsDocShellTreeOwner.h @@ -0,0 +1,111 @@ +/* -*- 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/. */ + +#ifndef nsDocShellTreeOwner_h__ +#define nsDocShellTreeOwner_h__ + +// Helper Classes +#include "nsCOMPtr.h" +#include "nsString.h" + +// Interfaces Needed +#include "nsIBaseWindow.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIWebBrowserChrome.h" +#include "nsIDOMEventListener.h" +#include "nsIWebProgressListener.h" +#include "nsWeakReference.h" +#include "nsITimer.h" +#include "nsIPrompt.h" +#include "nsIAuthPrompt.h" +#include "nsITooltipTextProvider.h" +#include "nsCTooltipTextProvider.h" + +namespace mozilla { +namespace dom { +class Event; +class EventTarget; +} // namespace dom +} // namespace mozilla + +class nsIDocShellTreeItem; +class nsWebBrowser; +class ChromeTooltipListener; + +class nsDocShellTreeOwner final : public nsIDocShellTreeOwner, + public nsIBaseWindow, + public nsIInterfaceRequestor, + public nsIWebProgressListener, + public nsIDOMEventListener, + public nsSupportsWeakReference { + friend class nsWebBrowser; + + public: + NS_DECL_ISUPPORTS + + NS_DECL_NSIBASEWINDOW + NS_DECL_NSIDOCSHELLTREEOWNER + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIWEBPROGRESSLISTENER + + protected: + nsDocShellTreeOwner(); + virtual ~nsDocShellTreeOwner(); + + void WebBrowser(nsWebBrowser* aWebBrowser); + + nsWebBrowser* WebBrowser(); + NS_IMETHOD SetTreeOwner(nsIDocShellTreeOwner* aTreeOwner); + NS_IMETHOD SetWebBrowserChrome(nsIWebBrowserChrome* aWebBrowserChrome); + + NS_IMETHOD AddChromeListeners(); + NS_IMETHOD RemoveChromeListeners(); + + void EnsurePrompter(); + void EnsureAuthPrompter(); + + void AddToWatcher(); + void RemoveFromWatcher(); + + void EnsureContentTreeOwner(); + + // These helper functions return the correct instances of the requested + // interfaces. If the object passed to SetWebBrowserChrome() implements + // nsISupportsWeakReference, then these functions call QueryReferent on + // that object. Otherwise, they return an addrefed pointer. If the + // WebBrowserChrome object doesn't exist, they return nullptr. + already_AddRefed<nsIWebBrowserChrome> GetWebBrowserChrome(); + already_AddRefed<nsIBaseWindow> GetOwnerWin(); + already_AddRefed<nsIInterfaceRequestor> GetOwnerRequestor(); + + protected: + // Weak References + nsWebBrowser* mWebBrowser; + nsIDocShellTreeOwner* mTreeOwner; + nsIDocShellTreeItem* mPrimaryContentShell; + + nsIWebBrowserChrome* mWebBrowserChrome; + nsIBaseWindow* mOwnerWin; + nsIInterfaceRequestor* mOwnerRequestor; + + nsWeakPtr mWebBrowserChromeWeak; // nsIWebBrowserChrome + + // the objects that listen for chrome events like context menus and tooltips. + // They are separate objects to avoid circular references between |this| + // and the DOM. + RefPtr<ChromeTooltipListener> mChromeTooltipListener; + + RefPtr<nsDocShellTreeOwner> mContentTreeOwner; + + nsCOMPtr<nsIPrompt> mPrompter; + nsCOMPtr<nsIAuthPrompt> mAuthPrompter; + nsCOMPtr<nsIRemoteTab> mPrimaryRemoteTab; +}; + +#endif /* nsDocShellTreeOwner_h__ */ diff --git a/docshell/base/nsIContentViewer.idl b/docshell/base/nsIContentViewer.idl new file mode 100644 index 0000000000..4f9dab1c35 --- /dev/null +++ b/docshell/base/nsIContentViewer.idl @@ -0,0 +1,318 @@ +/* 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 "nsISupports.idl" + +interface nsIDocShell; +interface nsISHEntry; +interface nsIPrintSettings; +webidl Document; +webidl Node; + +%{ C++ +#include "mozilla/Maybe.h" +#include "nsTArray.h" +#include "nsRect.h" +#include "Units.h" + +class nsIWidget; +class nsPresContext; +class nsView; +class nsDOMNavigationTiming; +namespace mozilla { +class Encoding; +class PresShell; +namespace dom { +class WindowGlobalChild; +} // namespace dom +namespace layout { +class RemotePrintJobChild; +} // namespace layout +} // namespace mozilla +%} + +[ptr] native nsIWidgetPtr(nsIWidget); +[ref] native nsIntRectRef(nsIntRect); +[ptr] native nsPresContextPtr(nsPresContext); +[ptr] native nsViewPtr(nsView); +[ptr] native nsDOMNavigationTimingPtr(nsDOMNavigationTiming); +[ptr] native Encoding(const mozilla::Encoding); +[ptr] native PresShellPtr(mozilla::PresShell); +[ptr] native RemotePrintJobChildPtr(mozilla::layout::RemotePrintJobChild); +[ptr] native WindowGlobalChildPtr(mozilla::dom::WindowGlobalChild); + +[scriptable, builtinclass, uuid(2da17016-7851-4a45-a7a8-00b360e01595)] +interface nsIContentViewer : nsISupports +{ + [noscript] void init(in nsIWidgetPtr aParentWidget, + [const] in nsIntRectRef aBounds, + in WindowGlobalChildPtr aWindowActor); + + attribute nsIDocShell container; + + [noscript,notxpcom,nostdcall] void loadStart(in Document aDoc); + [can_run_script] void loadComplete(in nsresult aStatus); + [notxpcom,nostdcall] readonly attribute boolean loadCompleted; + + [notxpcom,nostdcall] readonly attribute boolean isStopped; + + /** + * aAction is passed to PermitUnload to indicate what action to take + * if a beforeunload handler wants to prompt the user. + * + * ePrompt: Prompt and return the user's choice (default). + * eDontPromptAndDontUnload: Don't prompt and return false (unload not permitted) + * if the document (or its children) asks us to prompt. + * eDontPromptAndUnload: Don't prompt and return true (unload permitted) no matter what. + * + * NOTE: Keep this in sync with PermitUnloadAction in WindowGlobalActors.webidl. + */ + cenum PermitUnloadAction : 8 { + ePrompt = 0, + eDontPromptAndDontUnload = 1, + eDontPromptAndUnload = 2 + }; + + /** + * The result of dispatching a "beforeunload" event. If `eAllowNavigation`, + * no "beforeunload" listener requested to prevent the navigation, or its + * request was ignored. If `eRequestBlockNavigation`, a listener did request + * to block the navigation, and the user should be prompted. + */ + cenum PermitUnloadResult : 8 { + eAllowNavigation = 0, + eRequestBlockNavigation = 1, + }; + + /** + * Overload PermitUnload method for C++ consumers with no aPermitUnloadFlags + * argument. + */ + %{C++ + nsresult PermitUnload(bool* canUnload) { + return PermitUnload(ePrompt, canUnload); + } + %} + + /** + * Checks if the document wants to prevent unloading by firing beforeunload on + * the document. + * The result is returned. + */ + boolean permitUnload([optional] in nsIContentViewer_PermitUnloadAction aAction); + + /** + * Exposes whether we're blocked in a call to permitUnload. + */ + readonly attribute boolean inPermitUnload; + + /** + * Dispatches the "beforeunload" event and returns the result, as documented + * in the `PermitUnloadResult` enum. + */ + [noscript,nostdcall,notxpcom] nsIContentViewer_PermitUnloadResult dispatchBeforeUnload(); + + /** + * Exposes whether we're in the process of firing the beforeunload event. + * In this case, the corresponding docshell will not allow navigation. + */ + readonly attribute boolean beforeUnloadFiring; + + [can_run_script] void pageHide(in boolean isUnload); + + /** + * All users of a content viewer are responsible for calling both + * close() and destroy(), in that order. + * + * close() should be called when the load of a new page for the next + * content viewer begins, and destroy() should be called when the next + * content viewer replaces this one. + * + * |historyEntry| sets the session history entry for the content viewer. If + * this is null, then Destroy() will be called on the document by close(). + * If it is non-null, the document will not be destroyed, and the following + * actions will happen when destroy() is called (*): + * - Sanitize() will be called on the viewer's document + * - The content viewer will set the contentViewer property on the + * history entry, and release its reference (ownership reversal). + * - hide() will be called, and no further destruction will happen. + * + * (*) unless the document is currently being printed, in which case + * it will never be saved in session history. + * + */ + void close(in nsISHEntry historyEntry); + void destroy(); + + void stop(); + + /** + * Returns the same thing as getDocument(), but for use from script + * only. C++ consumers should use getDocument(). + */ + readonly attribute Document DOMDocument; + + /** + * Returns DOMDocument without addrefing. + */ + [noscript,notxpcom,nostdcall] Document getDocument(); + + /** + * Allows setting the document. + */ + [noscript,nostdcall] void setDocument(in Document aDocument); + + [noscript] void getBounds(in nsIntRectRef aBounds); + [noscript] void setBounds([const] in nsIntRectRef aBounds); + /** + * The 'aFlags' argument to setBoundsWithFlags is a set of these bits. + */ + const unsigned long eDelayResize = 1; + [noscript] void setBoundsWithFlags([const] in nsIntRectRef aBounds, + in unsigned long aFlags); + + /** + * The previous content viewer, which has been |close|d but not + * |destroy|ed. + */ + [notxpcom,nostdcall] attribute nsIContentViewer previousViewer; + + void move(in long aX, in long aY); + + void show(); + void hide(); + + attribute boolean sticky; + + /** + * Attach the content viewer to its DOM window and docshell. + * @param aState A state object that might be useful in attaching the DOM + * window. + * @param aSHEntry The history entry that the content viewer was stored in. + * The entry must have the docshells for all of the child + * documents stored in its child shell list. + */ + void open(in nsISupports aState, in nsISHEntry aSHEntry); + + /** + * Clears the current history entry. This is used if we need to clear out + * the saved presentation state. + */ + void clearHistoryEntry(); + + /** + * Change the layout to view the document with page layout (like print preview), but + * dynamic and editable (like Galley layout). + */ + void setPageModeForTesting(in boolean aPageMode, + in nsIPrintSettings aPrintSettings); + + /** + * Sets the print settings for print / print-previewing a subdocument. + */ + [can_run_script] void setPrintSettingsForSubdocument(in nsIPrintSettings aPrintSettings, + in RemotePrintJobChildPtr aRemotePrintJob); + + /** + * Get the history entry that this viewer will save itself into when + * destroyed. Can return null + */ + readonly attribute nsISHEntry historyEntry; + + /** + * Indicates when we're in a state where content shouldn't be allowed to + * trigger a tab-modal prompt (as opposed to a window-modal prompt) because + * we're part way through some operation (eg beforeunload) that shouldn't be + * rentrant if the user closes the tab while the prompt is showing. + * See bug 613800. + */ + readonly attribute boolean isTabModalPromptAllowed; + + /** + * Returns whether this content viewer is in a hidden state. + * + * @note Only Gecko internal code should set the attribute! + */ + attribute boolean isHidden; + + // presShell can be null. + [notxpcom,nostdcall] readonly attribute PresShellPtr presShell; + // presContext can be null. + [notxpcom,nostdcall] readonly attribute nsPresContextPtr presContext; + // aDocument must not be null. + [noscript] void setDocumentInternal(in Document aDocument, + in boolean aForceReuseInnerWindow); + /** + * Find the view to use as the container view for MakeWindow. Returns + * null if this will be the root of a view manager hierarchy. In that + * case, if mParentWidget is null then this document should not even + * be displayed. + */ + [noscript,notxpcom,nostdcall] nsViewPtr findContainerView(); + /** + * Set collector for navigation timing data (load, unload events). + */ + [noscript,notxpcom,nostdcall] void setNavigationTiming(in nsDOMNavigationTimingPtr aTiming); + + /** + * The actual full zoom in effect, as modified by the device context. + * For a requested full zoom, the device context may choose a slightly + * different effectiveFullZoom to accomodate integer rounding of app units + * per dev pixel. This property returns the actual zoom amount in use, + * though it may not be good user experience to report that a requested zoom + * of 90% is actually 89.1%, for example. This value is provided primarily to + * support media queries of dppx values, because those queries are matched + * against the actual native device pixel ratio and the actual full zoom. + * + * You should only need this for testing. + */ + readonly attribute float deviceFullZoomForTest; + + /** + * Disable entire author style level (including HTML presentation hints), + * for this viewer but not any child viewers. + */ + attribute boolean authorStyleDisabled; + + /** + * Returns the preferred width and height of the content, constrained to the + * given maximum values. If either maxWidth or maxHeight is less than or + * equal to zero, that dimension is not constrained. + * + * If a pref width is provided, it is max'd with the min-content size. + * + * All input and output values are in CSS pixels. + */ + void getContentSize(in long maxWidth, + in long maxHeight, + in long prefWidth, + out long width, + out long height); + +%{C++ + mozilla::Maybe<mozilla::CSSIntSize> GetContentSize(int32_t aMaxWidth = 0, int32_t aMaxHeight = 0, int32_t aPrefWidth = 0) { + int32_t w = 0; + int32_t h = 0; + if (NS_SUCCEEDED(GetContentSize(aMaxWidth, aMaxHeight, aPrefWidth, &w, &h))) { + return mozilla::Some(mozilla::CSSIntSize(w, h)); + } + return mozilla::Nothing(); + } +%} + + [noscript, notxpcom] Encoding getReloadEncodingAndSource(out int32_t aSource); + [noscript, notxpcom] void setReloadEncodingAndSource(in Encoding aEncoding, in int32_t aSource); + [noscript, notxpcom] void forgetReloadEncoding(); +}; + +%{C++ +namespace mozilla { +namespace dom { + +using XPCOMPermitUnloadAction = nsIContentViewer::PermitUnloadAction; +using PermitUnloadResult = nsIContentViewer::PermitUnloadResult; + +} // namespace dom +} // namespace mozilla +%} diff --git a/docshell/base/nsIContentViewerEdit.idl b/docshell/base/nsIContentViewerEdit.idl new file mode 100644 index 0000000000..551222bbc2 --- /dev/null +++ b/docshell/base/nsIContentViewerEdit.idl @@ -0,0 +1,36 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "nsISupports.idl" + +webidl Node; + +[scriptable, uuid(35BE2D7E-F29B-48EC-BF7E-80A30A724DE3)] +interface nsIContentViewerEdit : nsISupports +{ + void clearSelection(); + void selectAll(); + + void copySelection(); + readonly attribute boolean copyable; + + void copyLinkLocation(); + readonly attribute boolean inLink; + + const long COPY_IMAGE_TEXT = 0x0001; + const long COPY_IMAGE_HTML = 0x0002; + const long COPY_IMAGE_DATA = 0x0004; + const long COPY_IMAGE_ALL = -1; + void copyImage(in long aCopyFlags); + readonly attribute boolean inImage; + + AString getContents(in string aMimeType, in boolean aSelectionOnly); + readonly attribute boolean canGetContents; + + // Set the node that will be the subject of the editing commands above. + // Usually this will be the node that was context-clicked. + void setCommandNode(in Node aNode); +}; diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl new file mode 100644 index 0000000000..68f32e968c --- /dev/null +++ b/docshell/base/nsIDocShell.idl @@ -0,0 +1,796 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "domstubs.idl" +#include "nsIDocShellTreeItem.idl" +#include "nsIRequest.idl" + +%{ C++ +#include "js/TypeDecls.h" +#include "mozilla/Maybe.h" +#include "mozilla/NotNull.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsIURI.h" +class nsCommandManager; +class nsPresContext; +class nsDocShellLoadState; +namespace mozilla { +class Encoding; +class HTMLEditor; +class PresShell; +namespace dom { +class BrowsingContext; +class ClientSource; +} // namespace dom +} +%} + +/** + * The nsIDocShell interface. + */ + +[ptr] native nsPresContext(nsPresContext); +[ptr] native nsCommandManager(nsCommandManager); +[ptr] native PresShell(mozilla::PresShell); +[ref] native MaybeURI(mozilla::Maybe<nsCOMPtr<nsIURI>>); +[ref] native Encoding(const mozilla::Encoding*); + native UniqueClientSource(mozilla::UniquePtr<mozilla::dom::ClientSource>); + +interface nsIURI; +interface nsIChannel; +interface nsIContentViewer; +interface nsIContentSecurityPolicy; +interface nsIEditor; +interface nsIEditingSession; +interface nsIInputStream; +interface nsIRequest; +interface nsISHEntry; +interface nsILayoutHistoryState; +interface nsISecureBrowserUI; +interface nsIScriptGlobalObject; +interface nsIStructuredCloneContainer; +interface nsIDOMStorage; +interface nsIPrincipal; +interface nsIPrivacyTransitionObserver; +interface nsIReflowObserver; +interface nsIScrollObserver; +interface nsIRemoteTab; +interface nsIBrowserChild; +interface nsICommandParams; +interface nsILoadURIDelegate; +native BrowserChildRef(already_AddRefed<nsIBrowserChild>); +native nsDocShellLoadStatePtr(nsDocShellLoadState*); + +webidl BrowsingContext; +webidl ContentFrameMessageManager; +webidl EventTarget; +webidl Document; + +[scriptable, builtinclass, uuid(049234fe-da10-478b-bc5d-bc6f9a1ba63d)] +interface nsIDocShell : nsIDocShellTreeItem +{ + void setCancelContentJSEpoch(in long aEpoch); + + /** + * Loads a given URI. This will give priority to loading the requested URI + * in the object implementing this interface. If it can't be loaded here + * however, the URL dispatcher will go through its normal process of content + * loading. + * + * @param aLoadState This is the extended load info for this load. + * @param aSetNavigating If we should set isNavigating to true while initiating + * the load. + */ + [noscript]void loadURI(in nsDocShellLoadStatePtr aLoadState, in boolean aSetNavigating); + + /** + * Do either a history.pushState() or history.replaceState() operation, + * depending on the value of aReplace. + */ + [implicit_jscontext] + void addState(in jsval aData, in AString aTitle, + in AString aURL, in boolean aReplace); + + /** + * Reset state to a new content model within the current document and the document + * viewer. Called by the document before initiating an out of band document.write(). + */ + void prepareForNewContentModel(); + + /** + * Helper for the session store to change the URI associated with the + * document. + */ + void setCurrentURIForSessionStore(in nsIURI aURI); + + /** + * Notify the associated content viewer and all child docshells that they are + * about to be hidden. If |isUnload| is true, then the document is being + * unloaded and all dynamic subframe history entries are removed as well. + * + * @param isUnload + * True to fire the unload event in addition to the pagehide event, + * and remove all dynamic subframe history entries. + */ + [noscript] void firePageHideNotification(in boolean isUnload); + + /** + * Presentation context for the currently loaded document. This may be null. + */ + [notxpcom,nostdcall] readonly attribute nsPresContext presContext; + + /** + * Presentation shell for the currently loaded document. This may be null. + */ + [notxpcom,nostdcall] readonly attribute PresShell presShell; + + /** + * Presentation shell for the oldest document, if this docshell is + * currently transitioning between documents. + */ + [notxpcom,nostdcall] readonly attribute PresShell eldestPresShell; + + /** + * Content Viewer that is currently loaded for this DocShell. This may + * change as the underlying content changes. + */ + [infallible] readonly attribute nsIContentViewer contentViewer; + + /** + * Get the id of the outer window that is or will be in this docshell. + */ + [infallible] readonly attribute unsigned long long outerWindowID; + + /** + * This attribute allows chrome to tie in to handle DOM events that may + * be of interest to chrome. + */ + attribute EventTarget chromeEventHandler; + + /** + * This allows chrome to set a custom User agent on a specific docshell + */ + attribute AString customUserAgent; + + /** + * Whether CSS error reporting is enabled. + */ + attribute boolean cssErrorReportingEnabled; + + /** + * Whether to allow plugin execution + */ + attribute boolean allowPlugins; + + /** + * Attribute stating if refresh based redirects can be allowed + */ + attribute boolean allowMetaRedirects; + + /** + * Attribute stating if it should allow subframes (framesets/iframes) or not + */ + attribute boolean allowSubframes; + + /** + * Attribute stating whether or not images should be loaded. + */ + attribute boolean allowImages; + + /** + * Attribute stating whether or not media (audio/video) should be loaded. + */ + [infallible] attribute boolean allowMedia; + + /** + * Attribute that determines whether DNS prefetch is allowed for this subtree + * of the docshell tree. Defaults to true. Setting this will make it take + * effect starting with the next document loaded in the docshell. + */ + attribute boolean allowDNSPrefetch; + + /** + * Attribute that determines whether window control (move/resize) is allowed. + */ + attribute boolean allowWindowControl; + + /** + * True if the docshell allows its content to be handled by a content listener + * other than the docshell itself, including the external helper app service, + * and false otherwise. Defaults to true. + */ + [infallible] attribute boolean allowContentRetargeting; + + /** + * True if new child docshells should allow content retargeting. + * Setting allowContentRetargeting also overwrites this value. + */ + [infallible] attribute boolean allowContentRetargetingOnChildren; + + /** + * Get an array of this docShell and its children. + * + * @param aItemType - Only include docShells of this type, or if typeAll, + * include all child shells. + * Uses types from nsIDocShellTreeItem. + * @param aDirection - Whether to enumerate forwards or backwards. + */ + + cenum DocShellEnumeratorDirection : 8 { + ENUMERATE_FORWARDS = 0, + ENUMERATE_BACKWARDS = 1 + }; + + Array<nsIDocShell> getAllDocShellsInSubtree(in long aItemType, + in nsIDocShell_DocShellEnumeratorDirection aDirection); + + /** + * The type of application that created this window. + * + * DO NOT DELETE, see bug 176166. For firefox, this value will always be + * UNKNOWN. However, it is used heavily in Thunderbird/comm-central and we + * don't really have a great replacement at the moment, so we'll just leave it + * here. + */ + cenum AppType : 8 { + APP_TYPE_UNKNOWN = 0, + APP_TYPE_MAIL = 1, + APP_TYPE_EDITOR = 2 + }; + + [infallible] attribute nsIDocShell_AppType appType; + + /** + * certain docshells (like the message pane) + * should not throw up auth dialogs + * because it can act as a password trojan + */ + attribute boolean allowAuth; + + /** + * Set/Get the document scale factor. When setting this attribute, a + * NS_ERROR_NOT_IMPLEMENTED error may be returned by implementations + * not supporting zoom. Implementations not supporting zoom should return + * 1.0 all the time for the Get operation. 1.0 by the way is the default + * of zoom. This means 100% of normal scaling or in other words normal size + * no zoom. + */ + attribute float zoom; + + /* + * Tells the docshell to offer focus to its tree owner. + * This is currently only necessary for embedding chrome. + * If forDocumentNavigation is true, then document navigation should be + * performed, where only the root of documents are selected. Otherwise, the + * next element in the parent should be returned. Returns true if focus was + * successfully taken by the tree owner. + */ + bool tabToTreeOwner(in boolean forward, in boolean forDocumentNavigation); + + /** + * Current busy state for DocShell + */ + cenum BusyFlags : 8 { + BUSY_FLAGS_NONE = 0, + BUSY_FLAGS_BUSY = 1, + BUSY_FLAGS_BEFORE_PAGE_LOAD = 2, + BUSY_FLAGS_PAGE_LOADING = 4, + }; + + [infallible] readonly attribute nsIDocShell_BusyFlags busyFlags; + + /** + * Load commands for the document + */ + cenum LoadCommand : 8 { + LOAD_CMD_NORMAL = 0x1, // Normal load + LOAD_CMD_RELOAD = 0x2, // Reload + LOAD_CMD_HISTORY = 0x4, // Load from history + LOAD_CMD_PUSHSTATE = 0x8, // History.pushState() + }; + + /* + * Attribute to access the loadtype for the document. LoadType Enum is + * defined in nsDocShellLoadTypes.h + */ + [infallible] attribute unsigned long loadType; + + /* + * Default load flags (as defined in nsIRequest) that will be set on all + * requests made by this docShell and propagated to all child docShells and + * to nsILoadGroup::defaultLoadFlags for the docShell's loadGroup. + * Default is no flags. Once set, only future requests initiated by the + * docShell are affected, so in general, these flags should be set before + * the docShell loads any content. + */ + attribute nsLoadFlags defaultLoadFlags; + + /* + * returns true if the docshell is being destroyed, false otherwise + */ + boolean isBeingDestroyed(); + + /* + * Returns true if the docshell is currently executing the onLoad Handler + */ + readonly attribute boolean isExecutingOnLoadHandler; + + attribute nsILayoutHistoryState layoutHistoryState; + + /** + * Object used to delegate URI loading to an upper context. + * Currently only set for GeckoView to allow handling of load requests + * at the application level. + */ + readonly attribute nsILoadURIDelegate loadURIDelegate; + + /** + * Cancel the XPCOM timers for each meta-refresh URI in this docshell, + * and this docshell's children, recursively. The meta-refresh timers can be + * restarted using resumeRefreshURIs(). If the timers are already suspended, + * this has no effect. + */ + void suspendRefreshURIs(); + + /** + * Restart the XPCOM timers for each meta-refresh URI in this docshell, + * and this docshell's children, recursively. If the timers are already + * running, this has no effect. + */ + void resumeRefreshURIs(); + + /** + * Begin firing WebProgressListener notifications for restoring a page + * presentation. |viewer| is the content viewer whose document we are + * starting to load. If null, it defaults to the docshell's current content + * viewer, creating one if necessary. |top| should be true for the toplevel + * docshell that is being restored; it will be set to false when this method + * is called for child docshells. This method will post an event to + * complete the simulated load after returning to the event loop. + */ + void beginRestore(in nsIContentViewer viewer, in boolean top); + + /** + * Finish firing WebProgressListener notifications and DOM events for + * restoring a page presentation. This should only be called via + * beginRestore(). + */ + void finishRestore(); + + void clearCachedUserAgent(); + + void clearCachedPlatform(); + + /* Track whether we're currently restoring a document presentation. */ + readonly attribute boolean restoringDocument; + + /* attribute to access whether error pages are enabled */ + attribute boolean useErrorPages; + + /** + * Display a load error in a frame while keeping that frame's currentURI + * pointing correctly to the page where the error ocurred, rather than to + * the error document page. You must provide either the aURI or aURL parameter. + * + * @param aError The error code to be displayed + * @param aURI nsIURI of the page where the error happened + * @param aURL wstring of the page where the error happened + * @param aFailedChannel The channel related to this error + * + * Returns whether or not we displayed an error page (note: will always + * return false if in-content error pages are disabled!) + */ + boolean displayLoadError(in nsresult aError, + in nsIURI aURI, + in wstring aURL, + [optional] in nsIChannel aFailedChannel); + + /** + * The channel that failed to load and resulted in an error page. + * May be null. Relevant only to error pages. + */ + readonly attribute nsIChannel failedChannel; + + /** + * Keeps track of the previous nsISHEntry index and the current + * nsISHEntry index at the time that the doc shell begins to load. + * Used for ContentViewer eviction. + */ + readonly attribute long previousEntryIndex; + readonly attribute long loadedEntryIndex; + + /** + * Notification that entries have been removed from the beginning of a + * nsSHistory which has this as its rootDocShell. + * + * @param numEntries - The number of entries removed + */ + void historyPurged(in long numEntries); + + /** + * Gets the channel for the currently loaded document, if any. + * For a new document load, this will be the channel of the previous document + * until after OnLocationChange fires. + */ + readonly attribute nsIChannel currentDocumentChannel; + + /** + * Find out whether the docshell is currently in the middle of a page + * transition. This is set just before the pagehide/unload events fire. + */ + [infallible] readonly attribute boolean isInUnload; + + /** + * Disconnects this docshell's editor from its window, and stores the + * editor data in the open document's session history entry. This + * should be called only during page transitions. + */ + [noscript, notxpcom] void DetachEditorFromWindow(); + + /** + * Propagated to the print preview document viewer. Must only be called on + * a document viewer that has been initialized for print preview. + */ + void exitPrintPreview(); + + /** + * The ID of the docshell in the session history. + */ + readonly attribute nsIDRef historyID; + + /** + * Helper method for accessing this value from C++ + */ + [noscript, notxpcom] nsIDRef HistoryID(); + + /** + * Create a new about:blank document and content viewer. + * @param aPrincipal the principal to use for the new document. + * @param aPartitionedPrincipal the partitioned principal to use for the new + * document. + * @param aCsp the CSP to use for the new document. + */ + void createAboutBlankContentViewer(in nsIPrincipal aPrincipal, + in nsIPrincipal aPartitionedPrincipal, + [optional] in nsIContentSecurityPolicy aCSP); + + /** + * Upon getting, returns the canonical encoding label of the document + * currently loaded into this docshell. + */ + readonly attribute ACString charset; + + void forceEncodingDetection(); + + /** + * In a child docshell, this is the charset of the parent docshell + */ + [noscript, notxpcom, nostdcall] void setParentCharset( + in Encoding parentCharset, + in int32_t parentCharsetSource, + in nsIPrincipal parentCharsetPrincipal); + [noscript, notxpcom, nostdcall] void getParentCharset( + out Encoding parentCharset, + out int32_t parentCharsetSource, + out nsIPrincipal parentCharsetPrincipal); + + /** + * Whether the docShell records profile timeline markers at the moment + */ + [infallible] attribute boolean recordProfileTimelineMarkers; + + /** + * Return a DOMHighResTimeStamp representing the number of + * milliseconds from an arbitrary point in time. The reference + * point is shared by all DocShells and is also used by timestamps + * on markers. + */ + DOMHighResTimeStamp now(); + + /** + * Returns and flushes the profile timeline markers gathered by the docShell + */ + [implicit_jscontext] + jsval popProfileTimelineMarkers(); + + /** + * Add an observer to the list of parties to be notified when this docshell's + * private browsing status is changed. |obs| must support weak references. + */ + void addWeakPrivacyTransitionObserver(in nsIPrivacyTransitionObserver obs); + + /** + * Add an observer to the list of parties to be notified when reflows are + * occurring. |obs| must support weak references. + */ + void addWeakReflowObserver(in nsIReflowObserver obs); + + /** + * Remove an observer from the list of parties to be notified about reflows. + */ + void removeWeakReflowObserver(in nsIReflowObserver obs); + + /** + * Notify all attached observers that a reflow has just occurred. + * + * @param interruptible if true, the reflow was interruptible. + * @param start timestamp when reflow started, in milliseconds since + * navigationStart (accurate to 1/1000 of a ms) + * @param end timestamp when reflow ended, in milliseconds since + * navigationStart (accurate to 1/1000 of a ms) + */ + [noscript] void notifyReflowObservers(in bool interruptible, + in DOMHighResTimeStamp start, + in DOMHighResTimeStamp end); + + /** + * Add an observer to the list of parties to be notified when scroll position + * of some elements is changed. + */ + [noscript] void addWeakScrollObserver(in nsIScrollObserver obs); + + /** + * Add an observer to the list of parties to be notified when scroll position + * of some elements is changed. + */ + [noscript] void removeWeakScrollObserver(in nsIScrollObserver obs); + + /** + * Notify all attached observers that the scroll position of some element + * has changed. + */ + [noscript] void notifyScrollObservers(); + + /** + * Returns true if this docshell is the top level content docshell. + */ + [infallible] readonly attribute boolean isTopLevelContentDocShell; + + /** + * Like nsIDocShellTreeItem::GetSameTypeParent, except this ignores <iframe + * mozbrowser> boundaries. Which no longer exist. + * + * @deprecated: Use `BrowsingContext::GetParent()` in the future. + */ + nsIDocShell getSameTypeInProcessParentIgnoreBrowserBoundaries(); + + /** + * True iff asynchronous panning and zooming is enabled for this + * docshell. + */ + readonly attribute bool asyncPanZoomEnabled; + + /** + * Are plugins allowed in the current document loaded in this docshell ? + * (if there is one). This depends on whether plugins are allowed by this + * docshell itself or if the document is sandboxed and hence plugins should + * not be allowed. + */ + [noscript, notxpcom] bool pluginsAllowedInCurrentDoc(); + + /** + * Indicates whether the UI may enable the character encoding menu. The UI + * must disable the menu when this property is false. + */ + [infallible] readonly attribute boolean mayEnableCharacterEncodingMenu; + + attribute nsIEditor editor; + readonly attribute boolean editable; /* this docShell is editable */ + readonly attribute boolean hasEditingSession; /* this docShell has an editing session */ + + /** + * Make this docShell editable, setting a flag that causes + * an editor to get created, either immediately, or after + * a url has been loaded. + * @param inWaitForUriLoad true to wait for a URI before + * creating the editor. + */ + void makeEditable(in boolean inWaitForUriLoad); + + /** + * Returns false for mLSHE, true for mOSHE + */ + boolean getCurrentSHEntry(out nsISHEntry aEntry); + + /** + * Cherry picked parts of nsIController. + * They are here, because we want to call these functions + * from JS. + */ + boolean isCommandEnabled(in string command); + [can_run_script] + void doCommand(in string command); + [can_run_script] + void doCommandWithParams(in string command, in nsICommandParams aParams); + + /** + * Invisible DocShell are dummy construct to simulate DOM windows + * without any actual visual representation. They have to be marked + * at construction time, to avoid any painting activity. + */ + [noscript, notxpcom] bool IsInvisible(); + [noscript, notxpcom] void SetInvisible(in bool aIsInvisibleDocshell); + +/** + * Get the script global for the document in this docshell. +*/ + [noscript,notxpcom,nostdcall] nsIScriptGlobalObject GetScriptGlobalObject(); + + [noscript,notxpcom,nostdcall] Document getExtantDocument(); + + /** + * Notify DocShell when the browser is about to start executing JS, and after + * that execution has stopped. This only occurs when the Timeline devtool + * is collecting information. + */ + [noscript,notxpcom,nostdcall] void notifyJSRunToCompletionStart(in string aReason, + in AString functionName, + in AString fileName, + in unsigned long lineNumber, + in jsval asyncStack, + in string asyncCause); + [noscript,notxpcom,nostdcall] void notifyJSRunToCompletionStop(); + + /** + * This attribute determines whether a document which is not about:blank has + * already be loaded by this docShell. + */ + [infallible] readonly attribute boolean hasLoadedNonBlankURI; + + /** + * Allow usage of -moz-window-dragging:drag for content docshells. + * True for top level chrome docshells. Throws if set to false with + * top level chrome docshell. + */ + attribute boolean windowDraggingAllowed; + + /** + * Sets/gets the current scroll restoration mode. + * @see https://html.spec.whatwg.org/#dom-history-scroll-restoration + */ + attribute boolean currentScrollRestorationIsManual; + + /** + * Setter and getter for the origin attributes living on this docshell. + */ + [implicit_jscontext] + jsval getOriginAttributes(); + + [implicit_jscontext] + void setOriginAttributes(in jsval aAttrs); + + /** + * The editing session for this docshell. + */ + readonly attribute nsIEditingSession editingSession; + + /** + * The browser child for this docshell. + */ + [binaryname(ScriptableBrowserChild)] readonly attribute nsIBrowserChild browserChild; + [noscript,notxpcom,nostdcall] BrowserChildRef GetBrowserChild(); + + [noscript,nostdcall,notxpcom] nsCommandManager GetCommandManager(); + + cenum MetaViewportOverride: 8 { + /** + * Override platform/pref default behaviour and force-disable support for + * <meta name="viewport">. + */ + META_VIEWPORT_OVERRIDE_DISABLED = 0, + /** + * Override platform/pref default behaviour and force-enable support for + * <meta name="viewport">. + */ + META_VIEWPORT_OVERRIDE_ENABLED = 1, + /** + * Don't override the platform/pref default behaviour for support for + * <meta name="viewport">. + */ + META_VIEWPORT_OVERRIDE_NONE = 2, + }; + + /** + * This allows chrome to override the default choice of whether the + * <meta name="viewport"> tag is respected in a specific docshell. + * Possible values are listed above. + */ + [infallible, setter_can_run_script] attribute nsIDocShell_MetaViewportOverride metaViewportOverride; + + /** + * Attribute that determines whether tracking protection is enabled. + */ + attribute boolean useTrackingProtection; + + /** + * Fire a dummy location change event asynchronously. + */ + [noscript] void dispatchLocationChangeEvent(); + + + /** + * Start delayed autoplay media which are in the current document. + */ + [noscript] void startDelayedAutoplayMediaComponents(); + + /** + * Take ownership of the ClientSource representing an initial about:blank + * document that was never needed. As an optimization we avoid creating + * this document if no code calls GetDocument(), but we still need a + * ClientSource object to represent the about:blank window. This may return + * nullptr; for example if the docshell has created a real window and document + * already. + */ + [noscript, nostdcall, notxpcom] + UniqueClientSource TakeInitialClientSource(); + + void setColorMatrix(in Array<float> aMatrix); + + /** + * Returns true if the current load is a forced reload, + * e.g. started by holding shift whilst triggering reload. + */ + readonly attribute bool isForceReloading; + + Array<float> getColorMatrix(); + +%{C++ + /** + * These methods call nsDocShell::GetHTMLEditorInternal() and + * nsDocShell::SetHTMLEditorInternal() with static_cast. + */ + mozilla::HTMLEditor* GetHTMLEditor(); + nsresult SetHTMLEditor(mozilla::HTMLEditor* aHTMLEditor); +%} + + /** + * The message manager for this docshell. This does not throw, but + * can return null if the docshell has no message manager. + */ + [infallible] readonly attribute ContentFrameMessageManager messageManager; + + /** + * This returns a Promise which resolves to a boolean. True when the + * document has Tracking Content that has been blocked from loading, false + * otherwise. + */ + Promise getHasTrackingContentBlocked(); + + /** + * Return whether this docshell is "attempting to navigate" in the + * sense that's relevant to document.open. + */ + [notxpcom, nostdcall] readonly attribute boolean isAttemptingToNavigate; + + /* + * Whether or not this docshell is executing a nsIWebNavigation navigation + * method. + * + * This will be true when the following methods are executing: + * nsIWebNavigation.binaryLoadURI + * nsIWebNavigation.goBack + * nsIWebNavigation.goForward + * nsIWebNavigation.gotoIndex + * nsIWebNavigation.loadURI + */ + [infallible] readonly attribute boolean isNavigating; + + /** + * @see nsISHEntry synchronizeLayoutHistoryState(). + */ + void synchronizeLayoutHistoryState(); + + /** + * This attempts to save any applicable layout history state (like + * scroll position) in the nsISHEntry. This is normally done + * automatically when transitioning from page to page in the + * same process. We expose this function to support transitioning + * from page to page across processes as a workaround for bug 1630234 + * until session history state is moved into the parent process. + */ + void persistLayoutHistoryState(); +}; diff --git a/docshell/base/nsIDocShellTreeItem.idl b/docshell/base/nsIDocShellTreeItem.idl new file mode 100644 index 0000000000..a80373832f --- /dev/null +++ b/docshell/base/nsIDocShellTreeItem.idl @@ -0,0 +1,171 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "nsISupports.idl" + +interface mozIDOMWindowProxy; +interface nsIDocShellTreeOwner; +interface nsPIDOMWindowOuter; + +webidl Document; +webidl BrowsingContext; + +/** + * The nsIDocShellTreeItem supplies the methods that are required of any item + * that wishes to be able to live within the docshell tree either as a middle + * node or a leaf. + */ + +[scriptable, builtinclass, uuid(9b7c586f-9214-480c-a2c4-49b526fff1a6)] +interface nsIDocShellTreeItem : nsISupports +{ + /* + name of the DocShellTreeItem + */ + attribute AString name; + + /** + * Compares the provided name against the item's name and + * returns the appropriate result. + * + * @return <CODE>PR_TRUE</CODE> if names match; + * <CODE>PR_FALSE</CODE> otherwise. + */ + boolean nameEquals(in AString name); + + /* + Definitions for the item types. + */ + const long typeChrome=0; // typeChrome must equal 0 + const long typeContent=1; // typeContent must equal 1 + const long typeContentWrapper=2; // typeContentWrapper must equal 2 + const long typeChromeWrapper=3; // typeChromeWrapper must equal 3 + + const long typeAll=0x7FFFFFFF; + + /* + The type this item is. + */ + readonly attribute long itemType; + [noscript,notxpcom,nostdcall] long ItemType(); + + /* + Parent DocShell. + + @deprecated: Use `BrowsingContext::GetParent()` instead. + (NOTE: `BrowsingContext::GetParent()` will not cross isolation boundaries) + */ + [binaryname(InProcessParent)] + readonly attribute nsIDocShellTreeItem parent; + + /* + This getter returns the same thing parent does however if the parent + is of a different itemType, or if the parent is an <iframe mozbrowser>. + It will instead return nullptr. This call is a convience function for + Ithose wishing to not cross the boundaries at which item types change. + + @deprecated: Use `BrowsingContext::GetParent()` instead. + */ + [binaryname(InProcessSameTypeParent)] + readonly attribute nsIDocShellTreeItem sameTypeParent; + + /* + Returns the root DocShellTreeItem. This is a convience equivalent to + getting the parent and its parent until there isn't a parent. + + @deprecated: Use `BrowsingContext::Top()` instead. + (NOTE: `BrowsingContext::Top()` will not cross isolation boundaries) + */ + [binaryname(InProcessRootTreeItem)] + readonly attribute nsIDocShellTreeItem rootTreeItem; + + /* + Returns the root DocShellTreeItem of the same type. This is a convience + equivalent to getting the parent of the same type and its parent until + there isn't a parent. + + @deprecated: Use `BrowsingContext::Top()` instead. + */ + [binaryname(InProcessSameTypeRootTreeItem)] + readonly attribute nsIDocShellTreeItem sameTypeRootTreeItem; + + /* + The owner of the DocShell Tree. This interface will be called upon when + the docshell has things it needs to tell to the owner of the docshell. + Note that docShell tree ownership does not cross tree types. Meaning + setting ownership on a chrome tree does not set ownership on the content + sub-trees. A given tree's boundaries are identified by the type changes. + Trees of different types may be connected, but should not be traversed + for things such as ownership. + + Note implementers of this interface should NOT effect the lifetime of the + parent DocShell by holding this reference as it creates a cycle. Owners + when releasing this interface should set the treeOwner to nullptr. + Implementers of this interface are guaranteed that when treeOwner is + set that the poitner is valid without having to addref. + + Further note however when others try to get the interface it should be + addref'd before handing it to them. + */ + readonly attribute nsIDocShellTreeOwner treeOwner; + [noscript] void setTreeOwner(in nsIDocShellTreeOwner treeOwner); + + /* + The current number of DocShells which are immediate children of the + this object. + + + @deprecated: Prefer using `BrowsingContext::Children()`, as this count will + not include out-of-process iframes. + */ + [binaryname(InProcessChildCount), infallible] + readonly attribute long childCount; + + /* + Add a new child DocShellTreeItem. Adds to the end of the list. + Note that this does NOT take a reference to the child. The child stays + alive only as long as it's referenced from outside the docshell tree. + + @throws NS_ERROR_ILLEGAL_VALUE if child corresponds to the same + object as this treenode or an ancestor of this treenode + @throws NS_ERROR_UNEXPECTED if this node is a leaf in the tree. + */ + [noscript] void addChild(in nsIDocShellTreeItem child); + + /* + Removes a child DocShellTreeItem. + + @throws NS_ERROR_UNEXPECTED if this node is a leaf in the tree. + */ + [noscript] void removeChild(in nsIDocShellTreeItem child); + + /** + * Return the child at the index requested. This is 0-based. + * + * @deprecated: Prefer using `BrowsingContext::Children()`, as this will not + * include out-of-process iframes. + * + * @throws NS_ERROR_UNEXPECTED if the index is out of range + */ + [binaryname(GetInProcessChildAt)] + nsIDocShellTreeItem getChildAt(in long index); + + /** + * BrowsingContext associated with the DocShell. + */ + [binaryname(BrowsingContextXPCOM)] + readonly attribute BrowsingContext browsingContext; + + [noscript,notxpcom,nostdcall] BrowsingContext getBrowsingContext(); + + /** + * Returns the DOM outer window for the content viewer. + */ + readonly attribute mozIDOMWindowProxy domWindow; + + [noscript,nostdcall,notxpcom] Document getDocument(); + [noscript,nostdcall,notxpcom] nsPIDOMWindowOuter getWindow(); +}; diff --git a/docshell/base/nsIDocShellTreeOwner.idl b/docshell/base/nsIDocShellTreeOwner.idl new file mode 100644 index 0000000000..db6faa59c9 --- /dev/null +++ b/docshell/base/nsIDocShellTreeOwner.idl @@ -0,0 +1,113 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "nsISupports.idl" + +/** + * The nsIDocShellTreeOwner + */ + +interface nsIDocShellTreeItem; +interface nsIRemoteTab; +webidl BrowsingContext; + +[scriptable, uuid(0e3dc4b1-4cea-4a37-af71-79f0afd07574)] +interface nsIDocShellTreeOwner : nsISupports +{ + /** + * Called when a content shell is added to the docshell tree. This is + * _only_ called for "root" content shells (that is, ones whose parent is a + * chrome shell). + * + * @param aContentShell the shell being added. + * @param aPrimary whether the shell is primary. + */ + void contentShellAdded(in nsIDocShellTreeItem aContentShell, + in boolean aPrimary); + + /** + * Called when a content shell is removed from the docshell tree. This is + * _only_ called for "root" content shells (that is, ones whose parent is a + * chrome shell). Note that if aContentShell was never added, + * contentShellRemoved should just do nothing. + * + * @param aContentShell the shell being removed. + */ + void contentShellRemoved(in nsIDocShellTreeItem aContentShell); + + /* + Returns the Primary Content Shell + */ + readonly attribute nsIDocShellTreeItem primaryContentShell; + + void remoteTabAdded(in nsIRemoteTab aTab, in boolean aPrimary); + void remoteTabRemoved(in nsIRemoteTab aTab); + + /* + In multiprocess case we may not have primaryContentShell but + primaryRemoteTab. + */ + readonly attribute nsIRemoteTab primaryRemoteTab; + + /* + Get the BrowsingContext associated with either the primary content shell or + primary remote tab, depending on which is available. + */ + readonly attribute BrowsingContext primaryContentBrowsingContext; + + /* + Tells the tree owner to size its window or parent window in such a way + that the shell passed along will be the size specified. + */ + [can_run_script] + void sizeShellTo(in nsIDocShellTreeItem shell, in long cx, in long cy); + + /* + Gets the size of the primary content area in device pixels. This should work + for both in-process and out-of-process content areas. + */ + void getPrimaryContentSize(out long width, out long height); + /* + Sets the size of the primary content area in device pixels. This should work + for both in-process and out-of-process content areas. + */ + void setPrimaryContentSize(in long width, in long height); + + /* + Gets the size of the root docshell in device pixels. + */ + void getRootShellSize(out long width, out long height); + /* + Sets the size of the root docshell in device pixels. + */ + void setRootShellSize(in long width, in long height); + + /* + Sets the persistence of different attributes of the window. + */ + void setPersistence(in boolean aPersistPosition, + in boolean aPersistSize, + in boolean aPersistSizeMode); + + /* + Gets the current persistence states of the window. + */ + void getPersistence(out boolean aPersistPosition, + out boolean aPersistSize, + out boolean aPersistSizeMode); + + /* + Gets the number of tabs currently open in our window, assuming + this tree owner has such a concept. + */ + readonly attribute unsigned long tabCount; + + /* + Returns true if there is a primary content shell or a primary + remote tab. + */ + readonly attribute bool hasPrimaryContent; +}; diff --git a/docshell/base/nsIDocumentLoaderFactory.idl b/docshell/base/nsIDocumentLoaderFactory.idl new file mode 100644 index 0000000000..cd08bc0f3f --- /dev/null +++ b/docshell/base/nsIDocumentLoaderFactory.idl @@ -0,0 +1,39 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "nsISupports.idl" + +interface nsIChannel; +interface nsIContentViewer; +interface nsIStreamListener; +interface nsIDocShell; +interface nsILoadGroup; +interface nsIPrincipal; + +webidl Document; + +/** + * To get a component that implements nsIDocumentLoaderFactory + * for a given mimetype, use nsICategoryManager to find an entry + * with the mimetype as its name in the category "Gecko-Content-Viewers". + * The value of the entry is the contractid of the component. + * The component is a service, so use GetService, not CreateInstance to get it. + */ + +[scriptable, uuid(e795239e-9d3c-47c4-b063-9e600fb3b287)] +interface nsIDocumentLoaderFactory : nsISupports { + nsIContentViewer createInstance(in string aCommand, + in nsIChannel aChannel, + in nsILoadGroup aLoadGroup, + in ACString aContentType, + in nsIDocShell aContainer, + in nsISupports aExtraInfo, + out nsIStreamListener aDocListenerResult); + + nsIContentViewer createInstanceForDocument(in nsISupports aContainer, + in Document aDocument, + in string aCommand); +}; diff --git a/docshell/base/nsILoadContext.idl b/docshell/base/nsILoadContext.idl new file mode 100644 index 0000000000..af71b96b34 --- /dev/null +++ b/docshell/base/nsILoadContext.idl @@ -0,0 +1,148 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: ft=cpp tw=78 sw=2 et ts=2 sts=2 cin + * 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 "nsISupports.idl" + +interface mozIDOMWindowProxy; + +webidl Element; + +[ref] native OriginAttributes(mozilla::OriginAttributes); + +%{C++ +#ifdef MOZILLA_INTERNAL_API +namespace mozilla { +class OriginAttributes; +} +#endif +%} + +/** + * An nsILoadContext represents the context of a load. This interface + * can be queried for various information about where the load is + * happening. + */ +[builtinclass, scriptable, uuid(2813a7a3-d084-4d00-acd0-f76620315c02)] +interface nsILoadContext : nsISupports +{ + /** + * associatedWindow is the window with which the load is associated, if any. + * Note that the load may be triggered by a document which is different from + * the document in associatedWindow, and in fact the source of the load need + * not be same-origin with the document in associatedWindow. This attribute + * may be null if there is no associated window. + */ + readonly attribute mozIDOMWindowProxy associatedWindow; + + /** + * topWindow is the top window which is of same type as associatedWindow. + * This is equivalent to associatedWindow.top, but is provided here as a + * convenience. All the same caveats as associatedWindow of apply, of + * course. This attribute may be null if there is no associated window. + */ + readonly attribute mozIDOMWindowProxy topWindow; + + /** + * topFrameElement is the <iframe>, <frame>, or <browser> element which + * contains the topWindow with which the load is associated. + * + * Note that we may have a topFrameElement even when we don't have an + * associatedWindow, if the topFrameElement's content lives out of process. + * topFrameElement is available in single-process and multiprocess contexts. + * Note that topFrameElement may be in chrome even when the nsILoadContext is + * associated with content. + */ + readonly attribute Element topFrameElement; + + /** + * True if the load context is content (as opposed to chrome). This is + * determined based on the type of window the load is performed in, NOT based + * on any URIs that might be around. + */ + readonly attribute boolean isContent; + + /* + * Attribute that determines if private browsing should be used. May not be + * changed after a document has been loaded in this context. + */ + attribute boolean usePrivateBrowsing; + + /** + * Attribute that determines if remote (out-of-process) tabs should be used. + */ + readonly attribute boolean useRemoteTabs; + + /** + * Determines if out-of-process iframes should be used. + */ + readonly attribute boolean useRemoteSubframes; + + /* + * Attribute that determines if tracking protection should be used. May not be + * changed after a document has been loaded in this context. + */ + attribute boolean useTrackingProtection; + +%{C++ + /** + * De-XPCOMed getter to make call-sites cleaner. + */ + bool UsePrivateBrowsing() + { + bool usingPB = false; + GetUsePrivateBrowsing(&usingPB); + return usingPB; + } + + bool UseRemoteTabs() + { + bool usingRT = false; + GetUseRemoteTabs(&usingRT); + return usingRT; + } + + bool UseRemoteSubframes() + { + bool usingRSF = false; + GetUseRemoteSubframes(&usingRSF); + return usingRSF; + } + + bool UseTrackingProtection() + { + bool usingTP = false; + GetUseTrackingProtection(&usingTP); + return usingTP; + } +%} + + /** + * Set the private browsing state of the load context, meant to be used internally. + */ + [noscript] void SetPrivateBrowsing(in boolean aInPrivateBrowsing); + + /** + * Set the remote tabs state of the load context, meant to be used internally. + */ + [noscript] void SetRemoteTabs(in boolean aUseRemoteTabs); + + /** + * Set the remote subframes bit of this load context. Exclusively meant to be used internally. + */ + [noscript] void SetRemoteSubframes(in boolean aUseRemoteSubframes); + + /** + * A dictionary of the non-default origin attributes associated with this + * nsILoadContext. + */ + [binaryname(ScriptableOriginAttributes), implicit_jscontext] + readonly attribute jsval originAttributes; + + /** + * The C++ getter for origin attributes. + */ + [noscript, notxpcom] void GetOriginAttributes(out OriginAttributes aAttrs); +}; diff --git a/docshell/base/nsILoadURIDelegate.idl b/docshell/base/nsILoadURIDelegate.idl new file mode 100644 index 0000000000..eb5d4cbaf5 --- /dev/null +++ b/docshell/base/nsILoadURIDelegate.idl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIURI; +interface nsIPrincipal; + +/** + * The nsILoadURIDelegate interface. + * Used for delegating URI loads to GeckoView's application, e.g., Custom Tabs + * or Progressive Web Apps. + */ +[scriptable, uuid(78e42d37-a34c-4d96-b901-25385669aba4)] +interface nsILoadURIDelegate : nsISupports +{ + /** + * Delegates page load error handling. This may be called for either top-level + * loads or subframes. + * + * @param aURI The URI that failed to load. + * @param aError The error code. + * @param aErrorModule The error module code. + + * Returns an error page URL to load, or null to show the default error page. + * No error page is shown at all if an error is thrown. + */ + nsIURI + handleLoadError(in nsIURI aURI, in nsresult aError, in short aErrorModule); +}; diff --git a/docshell/base/nsIPrivacyTransitionObserver.idl b/docshell/base/nsIPrivacyTransitionObserver.idl new file mode 100644 index 0000000000..c85d468d33 --- /dev/null +++ b/docshell/base/nsIPrivacyTransitionObserver.idl @@ -0,0 +1,11 @@ +/* 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 "nsISupports.idl" + +[scriptable, function, uuid(b4b1449d-0ef0-47f5-b62e-adc57fd49702)] +interface nsIPrivacyTransitionObserver : nsISupports +{ + void privateModeChanged(in bool enabled); +}; diff --git a/docshell/base/nsIReflowObserver.idl b/docshell/base/nsIReflowObserver.idl new file mode 100644 index 0000000000..fb602e2603 --- /dev/null +++ b/docshell/base/nsIReflowObserver.idl @@ -0,0 +1,31 @@ +/* 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 "domstubs.idl" + +[scriptable, uuid(832e692c-c4a6-11e2-8fd1-dce678957a39)] +interface nsIReflowObserver : nsISupports +{ + /** + * Called when an uninterruptible reflow has occurred. + * + * @param start timestamp when reflow ended, in milliseconds since + * navigationStart (accurate to 1/1000 of a ms) + * @param end timestamp when reflow ended, in milliseconds since + * navigationStart (accurate to 1/1000 of a ms) + */ + void reflow(in DOMHighResTimeStamp start, + in DOMHighResTimeStamp end); + + /** + * Called when an interruptible reflow has occurred. + * + * @param start timestamp when reflow ended, in milliseconds since + * navigationStart (accurate to 1/1000 of a ms) + * @param end timestamp when reflow ended, in milliseconds since + * navigationStart (accurate to 1/1000 of a ms) + */ + void reflowInterruptible(in DOMHighResTimeStamp start, + in DOMHighResTimeStamp end); +}; diff --git a/docshell/base/nsIRefreshURI.idl b/docshell/base/nsIRefreshURI.idl new file mode 100644 index 0000000000..a4a578a344 --- /dev/null +++ b/docshell/base/nsIRefreshURI.idl @@ -0,0 +1,52 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "nsISupports.idl" + +interface nsIChannel; +interface nsIPrincipal; +interface nsIURI; + +[scriptable, uuid(a5e61a3c-51bd-45be-ac0c-e87b71860656)] +interface nsIRefreshURI : nsISupports { + /** + * Load a uri after waiting for aMillis milliseconds (as a result of a + * meta refresh). If the docshell is busy loading a page currently, the + * refresh request will be queued and executed when the current load + * finishes. + * + * @param aUri The uri to refresh. + * @param aPrincipal The triggeringPrincipal for the refresh load + * May be null, in which case the principal of current document will be + * applied. + * @param aMillis The number of milliseconds to wait. + */ + void refreshURI(in nsIURI aURI, in nsIPrincipal aPrincipal, + in unsigned long aMillis); + + /** + * Loads a URI immediately as if it were a meta refresh. + * + * @param aURI The URI to refresh. + * @param aPrincipal The triggeringPrincipal for the refresh load + * May be null, in which case the principal of current document will be + * applied. + * @param aMillis The number of milliseconds by which this refresh would + * be delayed if it were not being forced. + */ + void forceRefreshURI(in nsIURI aURI, in nsIPrincipal aPrincipal, + in unsigned long aMillis); + + /** + * Cancels all timer loads. + */ + void cancelRefreshURITimers(); + + /** + * True when there are pending refreshes, false otherwise. + */ + readonly attribute boolean refreshPending; +}; diff --git a/docshell/base/nsIScrollObserver.h b/docshell/base/nsIScrollObserver.h new file mode 100644 index 0000000000..9ff89002f0 --- /dev/null +++ b/docshell/base/nsIScrollObserver.h @@ -0,0 +1,45 @@ +/* -*- 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/. */ + +#ifndef nsIScrollObserver_h___ +#define nsIScrollObserver_h___ + +#include "nsISupports.h" +#include "Units.h" + +// Must be kept in sync with xpcom/rust/xpcom/src/interfaces/nonidl.rs +#define NS_ISCROLLOBSERVER_IID \ + { \ + 0xaa5026eb, 0x2f88, 0x4026, { \ + 0xa4, 0x6b, 0xf4, 0x59, 0x6b, 0x4e, 0xdf, 0x00 \ + } \ + } + +class nsIScrollObserver : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_ISCROLLOBSERVER_IID) + + /** + * Called when the scroll position of some element has changed. + */ + virtual void ScrollPositionChanged() = 0; + + /** + * Called when an async panning/zooming transform has started being applied + * and passed the scroll offset + */ + MOZ_CAN_RUN_SCRIPT virtual void AsyncPanZoomStarted(){}; + + /** + * Called when an async panning/zooming transform is no longer applied + * and passed the scroll offset + */ + MOZ_CAN_RUN_SCRIPT virtual void AsyncPanZoomStopped(){}; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIScrollObserver, NS_ISCROLLOBSERVER_IID) + +#endif /* nsIScrollObserver_h___ */ diff --git a/docshell/base/nsITooltipListener.idl b/docshell/base/nsITooltipListener.idl new file mode 100644 index 0000000000..0498aca57d --- /dev/null +++ b/docshell/base/nsITooltipListener.idl @@ -0,0 +1,44 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +/** + * An optional interface for embedding clients wishing to receive + * notifications for when a tooltip should be displayed or removed. + * The embedder implements this interface on the web browser chrome + * object associated with the window that notifications are required + * for. + * + * @see nsITooltipTextProvider + */ +[scriptable, uuid(44b78386-1dd2-11b2-9ad2-e4eee2ca1916)] +interface nsITooltipListener : nsISupports +{ + /** + * Called when a tooltip should be displayed. + * + * @param aXCoords The tooltip left edge X coordinate. + * @param aYCoords The tooltip top edge Y coordinate. + * @param aTipText The text to display in the tooltip, typically obtained + * from the TITLE attribute of the node (or containing parent) + * over which the pointer has been positioned. + * @param aTipDir The direction (ltr or rtl) in which to display the text + * + * @note + * Coordinates are specified in device pixels, relative to the top-left + * corner of the browser area. + * + * @return <code>NS_OK</code> if the tooltip was displayed. + */ + void onShowTooltip(in long aXCoords, in long aYCoords, in AString aTipText, + in AString aTipDir); + + /** + * Called when the tooltip should be hidden, either because the pointer + * has moved or the tooltip has timed out. + */ + void onHideTooltip(); +}; diff --git a/docshell/base/nsITooltipTextProvider.idl b/docshell/base/nsITooltipTextProvider.idl new file mode 100644 index 0000000000..3afaddbe2a --- /dev/null +++ b/docshell/base/nsITooltipTextProvider.idl @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +webidl Node; + +/** + * An interface implemented by a tooltip text provider service. This + * service is called to discover what tooltip text is associated + * with the node that the pointer is positioned over. + * + * Embedders may implement and register their own tooltip text provider + * service if they wish to provide different tooltip text. + * + * The default service returns the text stored in the TITLE + * attribute of the node or a containing parent. + * + * @note + * The tooltip text provider service is registered with the contract + * defined in NS_TOOLTIPTEXTPROVIDER_CONTRACTID. + * + * @see nsITooltipListener + * @see nsIComponentManager + * @see Node + */ +[scriptable, uuid(b128a1e6-44f3-4331-8fbe-5af360ff21ee)] +interface nsITooltipTextProvider : nsISupports +{ + /** + * Called to obtain the tooltip text for a node. + * + * @arg aNode The node to obtain the text from. + * @arg aText The tooltip text. + * @arg aDirection The text direction (ltr or rtl) to use + * + * @return <CODE>PR_TRUE</CODE> if tooltip text is associated + * with the node and was returned in the aText argument; + * <CODE>PR_FALSE</CODE> otherwise. + */ + boolean getNodeText(in Node aNode, out wstring aText, out wstring aDirection); +}; diff --git a/docshell/base/nsIURIFixup.idl b/docshell/base/nsIURIFixup.idl new file mode 100644 index 0000000000..3ddbd6c29e --- /dev/null +++ b/docshell/base/nsIURIFixup.idl @@ -0,0 +1,205 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 "nsISupports.idl" + +interface nsIURI; +interface nsIInputStream; +interface nsIDNSListener; +webidl BrowsingContext; + +/** + * Interface indicating what we found/corrected when fixing up a URI + */ +[scriptable, uuid(4819f183-b532-4932-ac09-b309cd853be7)] +interface nsIURIFixupInfo : nsISupports +{ + /** + * Consumer that asked for fixed up URI. + */ + attribute BrowsingContext consumer; + + /** + * Our best guess as to what URI the consumer will want. Might + * be null if we couldn't salvage anything (for instance, because + * the input was invalid as a URI and FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP + * was not passed) + */ + attribute nsIURI preferredURI; + + /** + * The fixed-up original input, *never* using a keyword search. + * (might be null if the original input was not recoverable as + * a URL, e.g. "foo bar"!) + */ + attribute nsIURI fixedURI; + + /** + * The name of the keyword search provider used to provide a keyword search; + * empty string if no keyword search was done. + */ + attribute AString keywordProviderName; + + /** + * The keyword as used for the search (post trimming etc.) + * empty string if no keyword search was done. + */ + attribute AString keywordAsSent; + + /** + * Whether we changed the protocol instead of using one from the input as-is. + */ + attribute boolean fixupChangedProtocol; + + /** + * Whether we created an alternative URI. We might have added a prefix and/or + * suffix, the contents of which are controlled by the + * browser.fixup.alternate.prefix and .suffix prefs, with the defaults being + * "www." and ".com", respectively. + */ + attribute boolean fixupCreatedAlternateURI; + + /** + * The original input + */ + attribute AUTF8String originalInput; + + /** + * The POST data to submit with the returned URI (see nsISearchSubmission). + */ + attribute nsIInputStream postData; +}; + + +/** + * Interface implemented by objects capable of fixing up strings into URIs + */ +[scriptable, uuid(1da7e9d4-620b-4949-849a-1cd6077b1b2d)] +interface nsIURIFixup : nsISupports +{ + /** No fixup flags. */ + const unsigned long FIXUP_FLAG_NONE = 0; + + /** + * Allow the fixup to use a keyword lookup service to complete the URI. + * The fixup object implementer should honour this flag and only perform + * any lengthy keyword (or search) operation if it is set. + */ + const unsigned long FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP = 1; + + /** + * Tell the fixup to make an alternate URI from the input URI, for example + * to turn foo into www.foo.com. + */ + const unsigned long FIXUP_FLAGS_MAKE_ALTERNATE_URI = 2; + + /* + * Set when the fixup happens in a private context, the used search engine + * may differ in this case. Not all consumers care about this, because they + * may not want the url to be transformed in a search. + */ + const unsigned long FIXUP_FLAG_PRIVATE_CONTEXT = 4; + + /* + * Fix common scheme typos. + */ + const unsigned long FIXUP_FLAG_FIX_SCHEME_TYPOS = 8; + + /* + * Force the fixup to make an alternate URI from the input URI regardless + * of the browser.fixup.alternate.enabled preference. + */ + const unsigned long FIXUP_FLAG_FORCE_ALTERNATE_URI = 16; + + /** + * Tries to converts the specified string into a URI, first attempting + * to correct any errors in the syntax or other vagaries. + * It returns information about what it corrected + * (e.g. whether we could rescue the URI or "just" generated a keyword + * search URI instead). + * + * @param aURIText Candidate URI. + * @param aFixupFlags Flags that govern ways the URI may be fixed up. + * Defaults to FIXUP_FLAG_NONE. + */ + nsIURIFixupInfo getFixupURIInfo(in AUTF8String aURIText, + [optional] in unsigned long aFixupFlags); + + /** + * Convert load flags from nsIWebNavigation to URI fixup flags for use in + * getFixupURIInfo. + * + * @param aURIText Candidate URI; used for determining whether to + * allow keyword lookups. + * @param aDocShellFlags Load flags from nsIDocShell to convert. + */ + unsigned long webNavigationFlagsToFixupFlags( + in AUTF8String aURIText, in unsigned long aDocShellFlags); + + /** + * Converts the specified keyword string into a URI. Note that it's the + * caller's responsibility to check whether keywords are enabled and + * whether aKeyword is a sensible keyword. + * + * @param aKeyword The keyword string to convert into a URI + * @param aIsPrivateContext Whether this is invoked from a private context. + */ + nsIURIFixupInfo keywordToURI(in AUTF8String aKeyword, + [optional] in boolean aIsPrivateContext); + + /** + * Given a uri-like string with a protocol, attempt to fix and convert it + * into an instance of nsIURIFixupInfo. + * + * Differently from getFixupURIInfo, this assumes the input string is an + * http/https uri, and can add a prefix and/or suffix to its hostname. + * + * The scheme will be changed to the scheme defined in + * "browser.fixup.alternate.protocol", which is by default, https. + * + * If the prefix and suffix of the host are missing, it will add them to + * the host using the preferences "browser.fixup.alternate.prefix" and + * "browser.fixup.alternate.suffix" as references. + * + * If a hostname suffix is present, but the URI doesn't contain a prefix, + * it will add the prefix via "browser.fixup.alternate.prefix" + * + * @param aUriString The URI to fixup and convert. + * @returns nsIURIFixupInfo + * A nsIURIFixupInfo object with the property fixedURI + * which contains the modified URI. + * @throws NS_ERROR_FAILURE + * If aUriString is undefined, or the scheme is not + * http/https. + */ + nsIURIFixupInfo forceHttpFixup(in AUTF8String aUriString); + + /** + * With the host associated with the URI, use nsIDNSService to determine + * if an IP address can be found for this host. This method will ignore checking + * hosts that are IP addresses. If the host does not contain any periods, depending + * on the browser.urlbar.dnsResolveFullyQualifiedNames preference value, a period + * may be appended in order to make it a fully qualified domain name. + * + * @param aURI The URI to parse and pass into the DNS lookup. + * @param aListener The listener when the result from the lookup is available. + * @param aOriginAttributes The originAttributes to pass the DNS lookup. + * @throws NS_ERROR_FAILURE if aURI does not have a displayHost or asciiHost. + */ + void checkHost(in nsIURI aURI, + in nsIDNSListener aListener, + [optional] in jsval aOriginAttributes); + + /** + * Returns true if the specified domain is known and false otherwise. + * A known domain is relevant when we have a single word and can't be + * sure whether to treat the word as a host name or should instead be + * treated as a search term. + * + * @param aDomain A domain name to query. + */ + bool isDomainKnown(in AUTF8String aDomain); +}; diff --git a/docshell/base/nsIWebNavigation.idl b/docshell/base/nsIWebNavigation.idl new file mode 100644 index 0000000000..1800e7312e --- /dev/null +++ b/docshell/base/nsIWebNavigation.idl @@ -0,0 +1,415 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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 "nsISupports.idl" + +interface nsIInputStream; +interface nsISHistory; +interface nsIURI; +interface nsIPrincipal; +interface nsIChildSHistory; +webidl Document; + +%{ C++ +#include "mozilla/dom/ChildSHistory.h" +namespace mozilla { +namespace dom { +struct LoadURIOptions; +} // namespace dom +} // namespace mozilla +%} + +[ref] native LoadURIOptionsRef(const mozilla::dom::LoadURIOptions); + +/** + * The nsIWebNavigation interface defines an interface for navigating the web. + * It provides methods and attributes to direct an object to navigate to a new + * location, stop or restart an in process load, or determine where the object + * has previously gone. + * + * Even though this is builtinclass, most of the interface is also implemented + * in RemoteWebNavigation, so if this interface changes, the implementation + * there may also need to change. + */ +[scriptable, builtinclass, uuid(3ade79d4-8cb9-4952-b18d-4f9b63ca0d31)] +interface nsIWebNavigation : nsISupports +{ + /** + * Indicates if the object can go back. If true this indicates that + * there is back session history available for navigation. + */ + readonly attribute boolean canGoBack; + + /** + * Indicates if the object can go forward. If true this indicates that + * there is forward session history available for navigation + */ + readonly attribute boolean canGoForward; + + /** + * Tells the object to navigate to the previous session history item. When a + * page is loaded from session history, all content is loaded from the cache + * (if available) and page state (such as form values and scroll position) is + * restored. + * + * @param {boolean} aRequireUserInteraction + * Tells goBack to skip history items that did not record any user + * interaction on their corresponding document while they were active. + * This means in case of multiple entries mapping to the same document, + * each entry has to have been flagged with user interaction separately. + * If no items have user interaction, the function will fall back + * to the first session history entry. + * + * @param {boolean} aUserActivation + * Tells goBack that the call was triggered by a user action (e.g.: + * The user clicked the back button). + * + * @throw NS_ERROR_UNEXPECTED + * Indicates that the call was unexpected at this time, which implies + * that canGoBack is false. + */ + void goBack([optional] in boolean aRequireUserInteraction, [optional] in boolean aUserActivation); + + /** + * Tells the object to navigate to the next session history item. When a + * page is loaded from session history, all content is loaded from the cache + * (if available) and page state (such as form values and scroll position) is + * restored. + * + * @param {boolean} aRequireUserInteraction + * Tells goForward to skip history items that did not record any user + * interaction on their corresponding document while they were active. + * This means in case of multiple entries mapping to the same document, + * each entry has to have been flagged with user interaction separately. + * If no items have user interaction, the function will fall back + * to the latest session history entry. + * + * @param {boolean} aUserActivation + * Tells goForward that the call was triggered by a user action (e.g.: + * The user clicked the forward button). + * + * @throw NS_ERROR_UNEXPECTED + * Indicates that the call was unexpected at this time, which implies + * that canGoForward is false. + */ + void goForward([optional] in boolean aRequireUserInteraction, [optional] in boolean aUserActivation); + + /** + * Tells the object to navigate to the session history item at a given index. + * + * @param {boolean} aUserActivation + * Tells goForward that the call was triggered by a user action (e.g.: + * The user clicked the forward button). + * + * @throw NS_ERROR_UNEXPECTED + * Indicates that the call was unexpected at this time, which implies + * that session history entry at the given index does not exist. + */ + void gotoIndex(in long index, [optional] in boolean aUserActivation); + + /**************************************************************************** + * The following flags may be bitwise combined to form the load flags + * parameter passed to either the loadURI or reload method. Some of these + * flags are only applicable to loadURI. + */ + + /** + * This flags defines the range of bits that may be specified. Flags + * outside this range may be used, but may not be passed to Reload(). + */ + const unsigned long LOAD_FLAGS_MASK = 0xffff; + + /** + * This is the default value for the load flags parameter. + */ + const unsigned long LOAD_FLAGS_NONE = 0x0000; + + /** + * Flags 0x1, 0x2, 0x4, 0x8 are reserved for internal use by + * nsIWebNavigation implementations for now. + */ + + /** + * This flag specifies that the load should have the semantics of an HTML + * Meta-refresh tag (i.e., that the cache should be bypassed). This flag + * is only applicable to loadURI. + * XXX the meaning of this flag is poorly defined. + * XXX no one uses this, so we should probably deprecate and remove it. + */ + const unsigned long LOAD_FLAGS_IS_REFRESH = 0x0010; + + /** + * This flag specifies that the load should have the semantics of a link + * click. This flag is only applicable to loadURI. + * XXX the meaning of this flag is poorly defined. + */ + const unsigned long LOAD_FLAGS_IS_LINK = 0x0020; + + /** + * This flag specifies that history should not be updated. This flag is only + * applicable to loadURI. + */ + const unsigned long LOAD_FLAGS_BYPASS_HISTORY = 0x0040; + + /** + * This flag specifies that any existing history entry should be replaced. + * This flag is only applicable to loadURI. + */ + const unsigned long LOAD_FLAGS_REPLACE_HISTORY = 0x0080; + + /** + * This flag specifies that the local web cache should be bypassed, but an + * intermediate proxy cache could still be used to satisfy the load. + */ + const unsigned long LOAD_FLAGS_BYPASS_CACHE = 0x0100; + + /** + * This flag specifies that any intermediate proxy caches should be bypassed + * (i.e., that the content should be loaded from the origin server). + */ + const unsigned long LOAD_FLAGS_BYPASS_PROXY = 0x0200; + + /** + * This flag specifies that a reload was triggered as a result of detecting + * an incorrect character encoding while parsing a previously loaded + * document. + */ + const unsigned long LOAD_FLAGS_CHARSET_CHANGE = 0x0400; + + /** + * If this flag is set, Stop() will be called before the load starts + * and will stop both content and network activity (the default is to + * only stop network activity). Effectively, this passes the + * STOP_CONTENT flag to Stop(), in addition to the STOP_NETWORK flag. + */ + const unsigned long LOAD_FLAGS_STOP_CONTENT = 0x0800; + + /** + * A hint this load was prompted by an external program: take care! + */ + const unsigned long LOAD_FLAGS_FROM_EXTERNAL = 0x1000; + + /** + * This flag specifies that this is the first load in this object. + * Set with care, since setting incorrectly can cause us to assume that + * nothing was actually loaded in this object if the load ends up being + * handled by an external application. This flag must not be passed to + * Reload. + */ + const unsigned long LOAD_FLAGS_FIRST_LOAD = 0x4000; + + /** + * This flag specifies that the load should not be subject to popup + * blocking checks. This flag must not be passed to Reload. + */ + const unsigned long LOAD_FLAGS_ALLOW_POPUPS = 0x8000; + + /** + * This flag specifies that the URI classifier should not be checked for + * this load. This flag must not be passed to Reload. + */ + const unsigned long LOAD_FLAGS_BYPASS_CLASSIFIER = 0x10000; + + /** + * Force relevant cookies to be sent with this load even if normally they + * wouldn't be. + */ + const unsigned long LOAD_FLAGS_FORCE_ALLOW_COOKIES = 0x20000; + + /** + * Prevent the owner principal from being inherited for this load. + */ + const unsigned long LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL = 0x40000; + + /** + * Overwrite the returned error code with a specific result code + * when an error page is displayed. + */ + const unsigned long LOAD_FLAGS_ERROR_LOAD_CHANGES_RV = 0x80000; + + /** + * This flag specifies that the URI may be submitted to a third-party + * server for correction. This should only be applied to non-sensitive + * URIs entered by users. This flag must not be passed to Reload. + */ + const unsigned long LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP = 0x100000; + + /** + * This flag specifies that common scheme typos should be corrected. + */ + const unsigned long LOAD_FLAGS_FIXUP_SCHEME_TYPOS = 0x200000; + + /** + * Allows a top-level data: navigation to occur. E.g. view-image + * is an explicit user action which should be allowed. + */ + const unsigned long LOAD_FLAGS_FORCE_ALLOW_DATA_URI = 0x400000; + + /** + * This load is the result of an HTTP redirect. + */ + const unsigned long LOAD_FLAGS_IS_REDIRECT = 0x800000; + + /** + * These flags force TRR modes 1 or 3 for the load. + */ + const unsigned long LOAD_FLAGS_DISABLE_TRR = 0x1000000; + const unsigned long LOAD_FLAGS_FORCE_TRR = 0x2000000; + + /** + * This load should bypass the LoadURIDelegate.loadUri. + */ + const unsigned long LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE = 0x4000000; + + /** + * This load has a user activation. (e.g: reload button was clicked) + */ + const unsigned long LOAD_FLAGS_USER_ACTIVATION = 0x8000000; + + /** + * Loads a given URI. This will give priority to loading the requested URI + * in the object implementing this interface. If it can't be loaded here + * however, the URI dispatcher will go through its normal process of content + * loading. + * + * @param aURI + * The URI to load. + * @param aLoadURIOptions + * A JSObject defined in LoadURIOptions.webidl holding info like e.g. + * the triggeringPrincipal, the referrer info. + */ + [implicit_jscontext, binaryname(LoadURIFromScript)] + void loadURI(in nsIURI aURI, + in jsval aLoadURIOptions); + + /** + * Parse / fix up a URI out of the string and load it. + * This will give priority to loading the requested URI + * in the object implementing this interface. If it can't be loaded here + * however, the URI dispatcher will go through its normal process of content + * loading. + * + * @param aURIString + * The URI string to load. For HTTP and FTP URLs and possibly others, + * characters above U+007F will be converted to UTF-8 and then URL- + * escaped per the rules of RFC 2396. + * This method may use nsIURIFixup to try to fix up typos etc. in the + * input string based on the load flag arguments in aLoadURIOptions. + * It can even convert the input to a search results page using the + * default search service. + * If you have an nsIURI anyway, prefer calling `loadURI`, above. + * @param aLoadURIOptions + * A JSObject defined in LoadURIOptions.webidl holding info like e.g. + * the triggeringPrincipal, the referrer info. + */ + [implicit_jscontext, binaryname(FixupAndLoadURIStringFromScript)] + void fixupAndLoadURIString(in AString aURIString, + in jsval aLoadURIOptions); + + /** + * A C++ friendly version of loadURI + */ + [nostdcall, binaryname(LoadURI)] + void binaryLoadURI(in nsIURI aURI, + in LoadURIOptionsRef aLoadURIOptions); + + /** + * A C++ friendly version of fixupAndLoadURIString + */ + [nostdcall, binaryname(FixupAndLoadURIString)] + void binaryFixupAndLoadURIString(in AString aURIString, + in LoadURIOptionsRef aLoadURIOptions); + + /** + * Tells the Object to reload the current page. There may be cases where the + * user will be asked to confirm the reload (for example, when it is + * determined that the request is non-idempotent). + * + * @param aReloadFlags + * Flags modifying load behaviour. This parameter is a bitwise + * combination of the Load Flags defined above. (Undefined bits are + * reserved for future use.) Generally you will pass LOAD_FLAGS_NONE + * for this parameter. + * + * @throw NS_BINDING_ABORTED + * Indicating that the user canceled the reload. + */ + void reload(in unsigned long aReloadFlags); + + /**************************************************************************** + * The following flags may be passed as the stop flags parameter to the stop + * method defined on this interface. + */ + + /** + * This flag specifies that all network activity should be stopped. This + * includes both active network loads and pending META-refreshes. + */ + const unsigned long STOP_NETWORK = 0x01; + + /** + * This flag specifies that all content activity should be stopped. This + * includes animated images, plugins and pending Javascript timeouts. + */ + const unsigned long STOP_CONTENT = 0x02; + + /** + * This flag specifies that all activity should be stopped. + */ + const unsigned long STOP_ALL = 0x03; + + /** + * Stops a load of a URI. + * + * @param aStopFlags + * This parameter is one of the stop flags defined above. + */ + void stop(in unsigned long aStopFlags); + + /** + * Retrieves the current DOM document for the frame, or lazily creates a + * blank document if there is none. This attribute never returns null except + * for unexpected error situations. + */ + readonly attribute Document document; + + /** + * The currently loaded URI or null. + */ + readonly attribute nsIURI currentURI; + + /** + * The session history object used by this web navigation instance. This + * object will be a mozilla::dom::ChildSHistory object, but is returned as + * nsISupports so it can be called from JS code. + */ + [binaryname(SessionHistoryXPCOM)] + readonly attribute nsISupports sessionHistory; + + %{ C++ + /** + * Get the session history object used by this nsIWebNavigation instance. + * Use this method instead of the XPCOM method when getting the + * SessionHistory from C++ code. + */ + already_AddRefed<mozilla::dom::ChildSHistory> + GetSessionHistory() + { + nsCOMPtr<nsISupports> history; + GetSessionHistoryXPCOM(getter_AddRefs(history)); + return history.forget() + .downcast<mozilla::dom::ChildSHistory>(); + } + %} + + /** + * Resume a load which has been redirected from another process. + * + * A negative |aHistoryIndex| value corresponds to a non-history load being + * resumed. + */ + void resumeRedirectedLoad(in unsigned long long aLoadIdentifier, + in long aHistoryIndex); +}; diff --git a/docshell/base/nsIWebNavigationInfo.idl b/docshell/base/nsIWebNavigationInfo.idl new file mode 100644 index 0000000000..0c3d07a8ed --- /dev/null +++ b/docshell/base/nsIWebNavigationInfo.idl @@ -0,0 +1,55 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +/** + * The nsIWebNavigationInfo interface exposes a way to get information + * on the capabilities of Gecko webnavigation objects. + */ +[scriptable, uuid(62a93afb-93a1-465c-84c8-0432264229de)] +interface nsIWebNavigationInfo : nsISupports +{ + /** + * Returned by isTypeSupported to indicate lack of support for a type. + * @note this is guaranteed not to change, so that boolean tests can be done + * on the return value if isTypeSupported to detect whether a type is + * supported at all. + */ + const unsigned long UNSUPPORTED = 0; + + /** + * Returned by isTypeSupported to indicate that a type is supported as an + * image. + */ + const unsigned long IMAGE = 1; + + /** + * Returned by isTypeSupported to indicate that a type is a special NPAPI + * plugin that render as a transparent region (we do not support NPAPI + * plugins). + */ + const unsigned long FALLBACK = 2; + + /** + * @note Other return types may be added here in the future as they become + * relevant. + */ + + /** + * Returned by isTypeSupported to indicate that a type is supported via some + * other means. + */ + const unsigned long OTHER = 1 << 15; + + /** + * Query whether aType is supported. + * @param aType the MIME type in question. + * @return an enum value indicating whether and how aType is supported. + * @note This method may rescan plugins to ensure that they're properly + * registered for the types they support. + */ + unsigned long isTypeSupported(in ACString aType); +}; diff --git a/docshell/base/nsIWebPageDescriptor.idl b/docshell/base/nsIWebPageDescriptor.idl new file mode 100644 index 0000000000..866cb54e6b --- /dev/null +++ b/docshell/base/nsIWebPageDescriptor.idl @@ -0,0 +1,30 @@ +/* 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 "nsISupports.idl" +interface nsIDocShell; + +/** + * The nsIWebPageDescriptor interface allows content being displayed in one + * window to be loaded into another window without refetching it from the + * network. + */ + +[scriptable, uuid(6f30b676-3710-4c2c-80b1-0395fb26516e)] +interface nsIWebPageDescriptor : nsISupports +{ + /** + * Tells the object to load the page that otherDocShell is currently loading, + * or has loaded already, as view source, with the url being `aURL`. + * + * @throws NS_ERROR_FAILURE - NS_ERROR_INVALID_POINTER + */ + void loadPageAsViewSource(in nsIDocShell otherDocShell, in AString aURL); + + + /** + * Retrieves the page descriptor for the curent document. + * @note, currentDescriptor is currently always an nsISHEntry object or null. + */ + readonly attribute nsISupports currentDescriptor; +}; diff --git a/docshell/base/nsPingListener.cpp b/docshell/base/nsPingListener.cpp new file mode 100644 index 0000000000..23ac220489 --- /dev/null +++ b/docshell/base/nsPingListener.cpp @@ -0,0 +1,345 @@ +/* -*- 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/. */ + +#include "nsPingListener.h" + +#include "mozilla/Encoding.h" +#include "mozilla/Preferences.h" + +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Document.h" + +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIInputStream.h" +#include "nsIProtocolHandler.h" +#include "nsIUploadChannel2.h" + +#include "nsComponentManagerUtils.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsWhitespaceTokenizer.h" + +using namespace mozilla; +using namespace mozilla::dom; + +NS_IMPL_ISUPPORTS(nsPingListener, nsIStreamListener, nsIRequestObserver) + +//***************************************************************************** +// <a ping> support +//***************************************************************************** + +#define PREF_PINGS_ENABLED "browser.send_pings" +#define PREF_PINGS_MAX_PER_LINK "browser.send_pings.max_per_link" +#define PREF_PINGS_REQUIRE_SAME_HOST "browser.send_pings.require_same_host" + +// Check prefs to see if pings are enabled and if so what restrictions might +// be applied. +// +// @param maxPerLink +// This parameter returns the number of pings that are allowed per link click +// +// @param requireSameHost +// This parameter returns true if pings are restricted to the same host as +// the document in which the click occurs. If the same host restriction is +// imposed, then we still allow for pings to cross over to different +// protocols and ports for flexibility and because it is not possible to send +// a ping via FTP. +// +// @returns +// true if pings are enabled and false otherwise. +// +static bool PingsEnabled(int32_t* aMaxPerLink, bool* aRequireSameHost) { + bool allow = Preferences::GetBool(PREF_PINGS_ENABLED, false); + + *aMaxPerLink = 1; + *aRequireSameHost = true; + + if (allow) { + Preferences::GetInt(PREF_PINGS_MAX_PER_LINK, aMaxPerLink); + Preferences::GetBool(PREF_PINGS_REQUIRE_SAME_HOST, aRequireSameHost); + } + + return allow; +} + +// We wait this many milliseconds before killing the ping channel... +#define PING_TIMEOUT 10000 + +static void OnPingTimeout(nsITimer* aTimer, void* aClosure) { + nsILoadGroup* loadGroup = static_cast<nsILoadGroup*>(aClosure); + if (loadGroup) { + loadGroup->Cancel(NS_ERROR_ABORT); + } +} + +struct MOZ_STACK_CLASS SendPingInfo { + int32_t numPings; + int32_t maxPings; + bool requireSameHost; + nsIURI* target; + nsIReferrerInfo* referrerInfo; + nsIDocShell* docShell; +}; + +static void SendPing(void* aClosure, nsIContent* aContent, nsIURI* aURI, + nsIIOService* aIOService) { + SendPingInfo* info = static_cast<SendPingInfo*>(aClosure); + if (info->maxPings > -1 && info->numPings >= info->maxPings) { + return; + } + + Document* doc = aContent->OwnerDoc(); + + nsCOMPtr<nsIChannel> chan; + NS_NewChannel(getter_AddRefs(chan), aURI, doc, + info->requireSameHost + ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED + : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_PING, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, // aLoadFlags, + aIOService); + + if (!chan) { + return; + } + + // Don't bother caching the result of this URI load, but do not exempt + // it from Safe Browsing. + chan->SetLoadFlags(nsIRequest::INHIBIT_CACHING); + + nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan); + if (!httpChan) { + return; + } + + // This is needed in order for 3rd-party cookie blocking to work. + nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(httpChan); + nsresult rv; + if (httpInternal) { + rv = httpInternal->SetDocumentURI(doc->GetDocumentURI()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + rv = httpChan->SetRequestMethod("POST"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Remove extraneous request headers (to reduce request size) + rv = httpChan->SetRequestHeader("accept"_ns, ""_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = httpChan->SetRequestHeader("accept-language"_ns, ""_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = httpChan->SetRequestHeader("accept-encoding"_ns, ""_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Always send a Ping-To header. + nsAutoCString pingTo; + if (NS_SUCCEEDED(info->target->GetSpec(pingTo))) { + rv = httpChan->SetRequestHeader("Ping-To"_ns, pingTo, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + nsCOMPtr<nsIScriptSecurityManager> sm = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID); + + if (sm && info->referrerInfo) { + nsCOMPtr<nsIURI> referrer = info->referrerInfo->GetOriginalReferrer(); + bool referrerIsSecure = false; + uint32_t flags = nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY; + if (referrer) { + rv = NS_URIChainHasFlags(referrer, flags, &referrerIsSecure); + } + + // Default to sending less data if NS_URIChainHasFlags() fails. + referrerIsSecure = NS_FAILED(rv) || referrerIsSecure; + + bool isPrivateWin = false; + if (doc) { + isPrivateWin = + doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > 0; + } + + bool sameOrigin = NS_SUCCEEDED( + sm->CheckSameOriginURI(referrer, aURI, false, isPrivateWin)); + + // If both the address of the document containing the hyperlink being + // audited and "ping URL" have the same origin or the document containing + // the hyperlink being audited was not retrieved over an encrypted + // connection, send a Ping-From header. + if (sameOrigin || !referrerIsSecure) { + nsAutoCString pingFrom; + if (NS_SUCCEEDED(referrer->GetSpec(pingFrom))) { + rv = httpChan->SetRequestHeader("Ping-From"_ns, pingFrom, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + // If the document containing the hyperlink being audited was not retrieved + // over an encrypted connection and its address does not have the same + // origin as "ping URL", send a referrer. + if (!sameOrigin && !referrerIsSecure && info->referrerInfo) { + rv = httpChan->SetReferrerInfo(info->referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(httpChan); + if (!uploadChan) { + return; + } + + constexpr auto uploadData = "PING"_ns; + + nsCOMPtr<nsIInputStream> uploadStream; + rv = NS_NewCStringInputStream(getter_AddRefs(uploadStream), uploadData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + uploadChan->ExplicitSetUploadStream(uploadStream, "text/ping"_ns, + uploadData.Length(), "POST"_ns, false); + + // The channel needs to have a loadgroup associated with it, so that we can + // cancel the channel and any redirected channels it may create. + nsCOMPtr<nsILoadGroup> loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); + if (!loadGroup) { + return; + } + nsCOMPtr<nsIInterfaceRequestor> callbacks = do_QueryInterface(info->docShell); + loadGroup->SetNotificationCallbacks(callbacks); + chan->SetLoadGroup(loadGroup); + + RefPtr<nsPingListener> pingListener = new nsPingListener(); + chan->AsyncOpen(pingListener); + + // Even if AsyncOpen failed, we still count this as a successful ping. It's + // possible that AsyncOpen may have failed after triggering some background + // process that may have written something to the network. + info->numPings++; + + // Prevent ping requests from stalling and never being garbage collected... + if (NS_FAILED(pingListener->StartTimeout(doc->GetDocGroup()))) { + // If we failed to setup the timer, then we should just cancel the channel + // because we won't be able to ensure that it goes away in a timely manner. + chan->Cancel(NS_ERROR_ABORT); + return; + } + // if the channel openend successfully, then make the pingListener hold + // a strong reference to the loadgroup which is released in ::OnStopRequest + pingListener->SetLoadGroup(loadGroup); +} + +typedef void (*ForEachPingCallback)(void* closure, nsIContent* content, + nsIURI* uri, nsIIOService* ios); + +static void ForEachPing(nsIContent* aContent, ForEachPingCallback aCallback, + void* aClosure) { + // NOTE: Using nsIDOMHTMLAnchorElement::GetPing isn't really worth it here + // since we'd still need to parse the resulting string. Instead, we + // just parse the raw attribute. It might be nice if the content node + // implemented an interface that exposed an enumeration of nsIURIs. + + // Make sure we are dealing with either an <A> or <AREA> element in the HTML + // or XHTML namespace. + if (!aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area)) { + return; + } + + nsAutoString value; + aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ping, value); + if (value.IsEmpty()) { + return; + } + + nsCOMPtr<nsIIOService> ios = do_GetIOService(); + if (!ios) { + return; + } + + Document* doc = aContent->OwnerDoc(); + nsAutoCString charset; + doc->GetDocumentCharacterSet()->Name(charset); + + nsWhitespaceTokenizer tokenizer(value); + + while (tokenizer.hasMoreTokens()) { + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), tokenizer.nextToken(), charset.get(), + aContent->GetBaseURI()); + // if we can't generate a valid URI, then there is nothing to do + if (!uri) { + continue; + } + // Explicitly not allow loading data: URIs + if (!net::SchemeIsData(uri)) { + aCallback(aClosure, aContent, uri, ios); + } + } +} + +// Spec: http://whatwg.org/specs/web-apps/current-work/#ping +/*static*/ void nsPingListener::DispatchPings(nsIDocShell* aDocShell, + nsIContent* aContent, + nsIURI* aTarget, + nsIReferrerInfo* aReferrerInfo) { + SendPingInfo info; + + if (!PingsEnabled(&info.maxPings, &info.requireSameHost)) { + return; + } + if (info.maxPings == 0) { + return; + } + + info.numPings = 0; + info.target = aTarget; + info.referrerInfo = aReferrerInfo; + info.docShell = aDocShell; + + ForEachPing(aContent, SendPing, &info); +} + +nsPingListener::~nsPingListener() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } +} + +nsresult nsPingListener::StartTimeout(DocGroup* aDocGroup) { + NS_ENSURE_ARG(aDocGroup); + + return NS_NewTimerWithFuncCallback( + getter_AddRefs(mTimer), OnPingTimeout, mLoadGroup, PING_TIMEOUT, + nsITimer::TYPE_ONE_SHOT, "nsPingListener::StartTimeout", + aDocGroup->EventTargetFor(TaskCategory::Network)); +} + +NS_IMETHODIMP +nsPingListener::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } + +NS_IMETHODIMP +nsPingListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + uint32_t result; + return aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &result); +} + +NS_IMETHODIMP +nsPingListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + mLoadGroup = nullptr; + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + return NS_OK; +} diff --git a/docshell/base/nsPingListener.h b/docshell/base/nsPingListener.h new file mode 100644 index 0000000000..7cf6ff98b5 --- /dev/null +++ b/docshell/base/nsPingListener.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#ifndef nsPingListener_h__ +#define nsPingListener_h__ + +#include "nsIStreamListener.h" +#include "nsIReferrerInfo.h" +#include "nsCOMPtr.h" + +namespace mozilla { +namespace dom { +class DocGroup; +} +} // namespace mozilla + +class nsIContent; +class nsIDocShell; +class nsILoadGroup; +class nsITimer; +class nsIURI; + +class nsPingListener final : public nsIStreamListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsPingListener() {} + + void SetLoadGroup(nsILoadGroup* aLoadGroup) { mLoadGroup = aLoadGroup; } + + nsresult StartTimeout(mozilla::dom::DocGroup* aDocGroup); + + static void DispatchPings(nsIDocShell* aDocShell, nsIContent* aContent, + nsIURI* aTarget, nsIReferrerInfo* aReferrerInfo); + + private: + ~nsPingListener(); + + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsCOMPtr<nsITimer> mTimer; +}; + +#endif /* nsPingListener_h__ */ diff --git a/docshell/base/nsRefreshTimer.cpp b/docshell/base/nsRefreshTimer.cpp new file mode 100644 index 0000000000..867e3ba1cb --- /dev/null +++ b/docshell/base/nsRefreshTimer.cpp @@ -0,0 +1,49 @@ +/* -*- 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/. */ + +#include "nsRefreshTimer.h" + +#include "nsIURI.h" +#include "nsIPrincipal.h" + +#include "nsDocShell.h" + +NS_IMPL_ADDREF(nsRefreshTimer) +NS_IMPL_RELEASE(nsRefreshTimer) + +NS_INTERFACE_MAP_BEGIN(nsRefreshTimer) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsINamed) +NS_INTERFACE_MAP_END + +nsRefreshTimer::nsRefreshTimer(nsDocShell* aDocShell, nsIURI* aURI, + nsIPrincipal* aPrincipal, int32_t aDelay) + : mDocShell(aDocShell), + mURI(aURI), + mPrincipal(aPrincipal), + mDelay(aDelay) {} + +nsRefreshTimer::~nsRefreshTimer() {} + +NS_IMETHODIMP +nsRefreshTimer::Notify(nsITimer* aTimer) { + NS_ASSERTION(mDocShell, "DocShell is somehow null"); + + if (mDocShell && aTimer) { + // Get the delay count to determine load type + uint32_t delay = 0; + aTimer->GetDelay(&delay); + mDocShell->ForceRefreshURIFromTimer(mURI, mPrincipal, delay, aTimer); + } + return NS_OK; +} + +NS_IMETHODIMP +nsRefreshTimer::GetName(nsACString& aName) { + aName.AssignLiteral("nsRefreshTimer"); + return NS_OK; +} diff --git a/docshell/base/nsRefreshTimer.h b/docshell/base/nsRefreshTimer.h new file mode 100644 index 0000000000..423f8807eb --- /dev/null +++ b/docshell/base/nsRefreshTimer.h @@ -0,0 +1,39 @@ +/* -*- 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/. */ + +#ifndef nsRefreshTimer_h__ +#define nsRefreshTimer_h__ + +#include "nsINamed.h" +#include "nsITimer.h" + +#include "nsCOMPtr.h" + +class nsDocShell; +class nsIURI; +class nsIPrincipal; + +class nsRefreshTimer : public nsITimerCallback, public nsINamed { + public: + nsRefreshTimer(nsDocShell* aDocShell, nsIURI* aURI, nsIPrincipal* aPrincipal, + int32_t aDelay); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + int32_t GetDelay() { return mDelay; } + + RefPtr<nsDocShell> mDocShell; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIPrincipal> mPrincipal; + int32_t mDelay; + + private: + virtual ~nsRefreshTimer(); +}; + +#endif /* nsRefreshTimer_h__ */ diff --git a/docshell/base/nsWebNavigationInfo.cpp b/docshell/base/nsWebNavigationInfo.cpp new file mode 100644 index 0000000000..0339499697 --- /dev/null +++ b/docshell/base/nsWebNavigationInfo.cpp @@ -0,0 +1,64 @@ +/* -*- 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/. */ + +#include "nsWebNavigationInfo.h" + +#include "nsServiceManagerUtils.h" +#include "nsIDocumentLoaderFactory.h" +#include "nsContentUtils.h" +#include "imgLoader.h" + +NS_IMPL_ISUPPORTS(nsWebNavigationInfo, nsIWebNavigationInfo) + +NS_IMETHODIMP +nsWebNavigationInfo::IsTypeSupported(const nsACString& aType, + uint32_t* aIsTypeSupported) { + MOZ_ASSERT(aIsTypeSupported, "null out param?"); + + *aIsTypeSupported = IsTypeSupported(aType); + return NS_OK; +} + +uint32_t nsWebNavigationInfo::IsTypeSupported(const nsACString& aType) { + // We want to claim that the type for PDF documents is unsupported, + // so that the internal PDF viewer's stream converted will get used. + if (aType.LowerCaseEqualsLiteral("application/pdf") && + nsContentUtils::IsPDFJSEnabled()) { + return nsIWebNavigationInfo::UNSUPPORTED; + } + + const nsCString& flatType = PromiseFlatCString(aType); + return IsTypeSupportedInternal(flatType); +} + +uint32_t nsWebNavigationInfo::IsTypeSupportedInternal(const nsCString& aType) { + nsContentUtils::ContentViewerType vtype = nsContentUtils::TYPE_UNSUPPORTED; + + nsCOMPtr<nsIDocumentLoaderFactory> docLoaderFactory = + nsContentUtils::FindInternalContentViewer(aType, &vtype); + + switch (vtype) { + case nsContentUtils::TYPE_UNSUPPORTED: + return nsIWebNavigationInfo::UNSUPPORTED; + + case nsContentUtils::TYPE_FALLBACK: + return nsIWebNavigationInfo::FALLBACK; + + case nsContentUtils::TYPE_UNKNOWN: + return nsIWebNavigationInfo::OTHER; + + case nsContentUtils::TYPE_CONTENT: + // XXXbz we only need this because images register for the same + // contractid as documents, so we can't tell them apart based on + // contractid. + if (imgLoader::SupportImageWithMimeType(aType)) { + return nsIWebNavigationInfo::IMAGE; + } + return nsIWebNavigationInfo::OTHER; + } + + return nsIWebNavigationInfo::UNSUPPORTED; +} diff --git a/docshell/base/nsWebNavigationInfo.h b/docshell/base/nsWebNavigationInfo.h new file mode 100644 index 0000000000..97b07c825e --- /dev/null +++ b/docshell/base/nsWebNavigationInfo.h @@ -0,0 +1,34 @@ +/* -*- 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/. */ + +#ifndef nsWebNavigationInfo_h__ +#define nsWebNavigationInfo_h__ + +#include "nsIWebNavigationInfo.h" +#include "nsCOMPtr.h" +#include "nsICategoryManager.h" +#include "nsStringFwd.h" +#include "mozilla/Attributes.h" + +class nsWebNavigationInfo final : public nsIWebNavigationInfo { + public: + nsWebNavigationInfo() {} + + NS_DECL_ISUPPORTS + + NS_DECL_NSIWEBNAVIGATIONINFO + + static uint32_t IsTypeSupported(const nsACString& aType); + + private: + ~nsWebNavigationInfo() {} + + // Check whether aType is supported, and returns an nsIWebNavigationInfo + // constant. + static uint32_t IsTypeSupportedInternal(const nsCString& aType); +}; + +#endif // nsWebNavigationInfo_h__ diff --git a/docshell/base/timeline/AbstractTimelineMarker.cpp b/docshell/base/timeline/AbstractTimelineMarker.cpp new file mode 100644 index 0000000000..471742089c --- /dev/null +++ b/docshell/base/timeline/AbstractTimelineMarker.cpp @@ -0,0 +1,72 @@ +/* -*- 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/. */ + +#include "AbstractTimelineMarker.h" + +#include "mozilla/TimeStamp.h" +#include "MainThreadUtils.h" +#include "nsAppRunner.h" + +namespace mozilla { + +AbstractTimelineMarker::AbstractTimelineMarker(const char* aName, + MarkerTracingType aTracingType) + : mName(aName), + mTracingType(aTracingType), + mProcessType(XRE_GetProcessType()), + mIsOffMainThread(!NS_IsMainThread()) { + MOZ_COUNT_CTOR(AbstractTimelineMarker); + SetCurrentTime(); +} + +AbstractTimelineMarker::AbstractTimelineMarker(const char* aName, + const TimeStamp& aTime, + MarkerTracingType aTracingType) + : mName(aName), + mTracingType(aTracingType), + mProcessType(XRE_GetProcessType()), + mIsOffMainThread(!NS_IsMainThread()) { + MOZ_COUNT_CTOR(AbstractTimelineMarker); + SetCustomTime(aTime); +} + +UniquePtr<AbstractTimelineMarker> AbstractTimelineMarker::Clone() { + MOZ_ASSERT(false, "Clone method not yet implemented on this marker type."); + return nullptr; +} + +bool AbstractTimelineMarker::Equals(const AbstractTimelineMarker& aOther) { + // Check whether two markers should be considered the same, for the purpose + // of pairing start and end markers. Normally this definition suffices. + return strcmp(mName, aOther.mName) == 0; +} + +AbstractTimelineMarker::~AbstractTimelineMarker() { + MOZ_COUNT_DTOR(AbstractTimelineMarker); +} + +void AbstractTimelineMarker::SetCurrentTime() { + TimeStamp now = TimeStamp::Now(); + SetCustomTime(now); +} + +void AbstractTimelineMarker::SetCustomTime(const TimeStamp& aTime) { + mTime = (aTime - TimeStamp::ProcessCreation()).ToMilliseconds(); +} + +void AbstractTimelineMarker::SetCustomTime(DOMHighResTimeStamp aTime) { + mTime = aTime; +} + +void AbstractTimelineMarker::SetProcessType(GeckoProcessType aProcessType) { + mProcessType = aProcessType; +} + +void AbstractTimelineMarker::SetOffMainThread(bool aIsOffMainThread) { + mIsOffMainThread = aIsOffMainThread; +} + +} // namespace mozilla diff --git a/docshell/base/timeline/AbstractTimelineMarker.h b/docshell/base/timeline/AbstractTimelineMarker.h new file mode 100644 index 0000000000..8581ba53f8 --- /dev/null +++ b/docshell/base/timeline/AbstractTimelineMarker.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/. */ + +#ifndef mozilla_AbstractTimelineMarker_h_ +#define mozilla_AbstractTimelineMarker_h_ + +#include "TimelineMarkerEnums.h" // for MarkerTracingType +#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp +#include "nsXULAppAPI.h" // for GeckoProcessType +#include "mozilla/UniquePtr.h" + +struct JSContext; +class JSObject; + +namespace mozilla { +class TimeStamp; + +namespace dom { +struct ProfileTimelineMarker; +} + +class AbstractTimelineMarker { + private: + AbstractTimelineMarker() = delete; + AbstractTimelineMarker(const AbstractTimelineMarker& aOther) = delete; + void operator=(const AbstractTimelineMarker& aOther) = delete; + + public: + AbstractTimelineMarker(const char* aName, MarkerTracingType aTracingType); + + AbstractTimelineMarker(const char* aName, const TimeStamp& aTime, + MarkerTracingType aTracingType); + + virtual ~AbstractTimelineMarker(); + + virtual UniquePtr<AbstractTimelineMarker> Clone(); + virtual bool Equals(const AbstractTimelineMarker& aOther); + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) = 0; + virtual JSObject* GetStack() = 0; + + const char* GetName() const { return mName; } + DOMHighResTimeStamp GetTime() const { return mTime; } + MarkerTracingType GetTracingType() const { return mTracingType; } + + uint8_t GetProcessType() const { return mProcessType; }; + bool IsOffMainThread() const { return mIsOffMainThread; }; + + private: + const char* mName; + DOMHighResTimeStamp mTime; + MarkerTracingType mTracingType; + + uint8_t mProcessType; // @see `enum GeckoProcessType`. + bool mIsOffMainThread; + + protected: + void SetCurrentTime(); + void SetCustomTime(const TimeStamp& aTime); + void SetCustomTime(DOMHighResTimeStamp aTime); + void SetProcessType(GeckoProcessType aProcessType); + void SetOffMainThread(bool aIsOffMainThread); +}; + +} // namespace mozilla + +#endif /* mozilla_AbstractTimelineMarker_h_ */ diff --git a/docshell/base/timeline/AutoGlobalTimelineMarker.cpp b/docshell/base/timeline/AutoGlobalTimelineMarker.cpp new file mode 100644 index 0000000000..bf5a19cbab --- /dev/null +++ b/docshell/base/timeline/AutoGlobalTimelineMarker.cpp @@ -0,0 +1,39 @@ +/* -*- 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/. */ + +#include "AutoGlobalTimelineMarker.h" + +#include "TimelineConsumers.h" +#include "MainThreadUtils.h" + +namespace mozilla { + +AutoGlobalTimelineMarker::AutoGlobalTimelineMarker( + const char* aName, MarkerStackRequest aStackRequest /* = STACK */ + ) + : mName(aName), mStackRequest(aStackRequest) { + MOZ_ASSERT(NS_IsMainThread()); + + if (TimelineConsumers::IsEmpty()) { + return; + } + + TimelineConsumers::AddMarkerForAllObservedDocShells( + mName, MarkerTracingType::START, mStackRequest); +} + +AutoGlobalTimelineMarker::~AutoGlobalTimelineMarker() { + MOZ_ASSERT(NS_IsMainThread()); + + if (TimelineConsumers::IsEmpty()) { + return; + } + + TimelineConsumers::AddMarkerForAllObservedDocShells( + mName, MarkerTracingType::END, mStackRequest); +} + +} // namespace mozilla diff --git a/docshell/base/timeline/AutoGlobalTimelineMarker.h b/docshell/base/timeline/AutoGlobalTimelineMarker.h new file mode 100644 index 0000000000..a9bbc92fce --- /dev/null +++ b/docshell/base/timeline/AutoGlobalTimelineMarker.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#ifndef mozilla_AutoGlobalTimelineMarker_h_ +#define mozilla_AutoGlobalTimelineMarker_h_ + +#include "mozilla/Attributes.h" +#include "TimelineMarkerEnums.h" + +namespace mozilla { + +// # AutoGlobalTimelineMarker +// +// Similar to `AutoTimelineMarker`, but adds its traced marker to all docshells, +// not a single particular one. This is useful for operations that aren't +// associated with any one particular doc shell, or when it isn't clear which +// docshell triggered the operation. +// +// Example usage: +// +// { +// AutoGlobalTimelineMarker marker("Cycle Collection"); +// nsCycleCollector* cc = GetCycleCollector(); +// cc->Collect(); +// ... +// } +class MOZ_RAII AutoGlobalTimelineMarker { + // The name of the marker we are adding. + const char* mName; + // Whether to capture the JS stack or not. + MarkerStackRequest mStackRequest; + + public: + explicit AutoGlobalTimelineMarker( + const char* aName, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + ~AutoGlobalTimelineMarker(); + + AutoGlobalTimelineMarker(const AutoGlobalTimelineMarker& aOther) = delete; + void operator=(const AutoGlobalTimelineMarker& aOther) = delete; +}; + +} // namespace mozilla + +#endif /* mozilla_AutoGlobalTimelineMarker_h_ */ diff --git a/docshell/base/timeline/AutoRestyleTimelineMarker.cpp b/docshell/base/timeline/AutoRestyleTimelineMarker.cpp new file mode 100644 index 0000000000..d9992bc33f --- /dev/null +++ b/docshell/base/timeline/AutoRestyleTimelineMarker.cpp @@ -0,0 +1,51 @@ +/* -*- 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/. */ + +#include "AutoRestyleTimelineMarker.h" + +#include "TimelineConsumers.h" +#include "MainThreadUtils.h" +#include "nsIDocShell.h" +#include "RestyleTimelineMarker.h" + +namespace mozilla { + +AutoRestyleTimelineMarker::AutoRestyleTimelineMarker(nsIDocShell* aDocShell, + bool aIsAnimationOnly) + : mDocShell(nullptr), mIsAnimationOnly(aIsAnimationOnly) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aDocShell) { + return; + } + + if (!TimelineConsumers::HasConsumer(aDocShell)) { + return; + } + + mDocShell = aDocShell; + TimelineConsumers::AddMarkerForDocShell( + mDocShell, MakeUnique<RestyleTimelineMarker>(mIsAnimationOnly, + MarkerTracingType::START)); +} + +AutoRestyleTimelineMarker::~AutoRestyleTimelineMarker() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mDocShell) { + return; + } + + if (!TimelineConsumers::HasConsumer(mDocShell)) { + return; + } + + TimelineConsumers::AddMarkerForDocShell( + mDocShell, MakeUnique<RestyleTimelineMarker>(mIsAnimationOnly, + MarkerTracingType::END)); +} + +} // namespace mozilla diff --git a/docshell/base/timeline/AutoRestyleTimelineMarker.h b/docshell/base/timeline/AutoRestyleTimelineMarker.h new file mode 100644 index 0000000000..1246e1a9bd --- /dev/null +++ b/docshell/base/timeline/AutoRestyleTimelineMarker.h @@ -0,0 +1,30 @@ +/* -*- 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/. */ + +#ifndef mozilla_AutoRestyleTimelineMarker_h_ +#define mozilla_AutoRestyleTimelineMarker_h_ + +#include "mozilla/RefPtr.h" + +class nsIDocShell; + +namespace mozilla { + +class MOZ_RAII AutoRestyleTimelineMarker { + RefPtr<nsIDocShell> mDocShell; + bool mIsAnimationOnly; + + public: + AutoRestyleTimelineMarker(nsIDocShell* aDocShell, bool aIsAnimationOnly); + ~AutoRestyleTimelineMarker(); + + AutoRestyleTimelineMarker(const AutoRestyleTimelineMarker& aOther) = delete; + void operator=(const AutoRestyleTimelineMarker& aOther) = delete; +}; + +} // namespace mozilla + +#endif /* mozilla_AutoRestyleTimelineMarker_h_ */ diff --git a/docshell/base/timeline/AutoTimelineMarker.cpp b/docshell/base/timeline/AutoTimelineMarker.cpp new file mode 100644 index 0000000000..a826706d8e --- /dev/null +++ b/docshell/base/timeline/AutoTimelineMarker.cpp @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#include "AutoTimelineMarker.h" + +#include "nsIDocShell.h" +#include "TimelineConsumers.h" +#include "MainThreadUtils.h" + +namespace mozilla { + +AutoTimelineMarker::AutoTimelineMarker(nsIDocShell* aDocShell, + const char* aName) + : mName(aName), mDocShell(nullptr) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aDocShell) { + return; + } + + if (!TimelineConsumers::HasConsumer(aDocShell)) { + return; + } + + mDocShell = aDocShell; + TimelineConsumers::AddMarkerForDocShell(mDocShell, mName, + MarkerTracingType::START); +} + +AutoTimelineMarker::~AutoTimelineMarker() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mDocShell) { + return; + } + + if (!TimelineConsumers::HasConsumer(mDocShell)) { + return; + } + + TimelineConsumers::AddMarkerForDocShell(mDocShell, mName, + MarkerTracingType::END); +} + +} // namespace mozilla diff --git a/docshell/base/timeline/AutoTimelineMarker.h b/docshell/base/timeline/AutoTimelineMarker.h new file mode 100644 index 0000000000..a86c741bb0 --- /dev/null +++ b/docshell/base/timeline/AutoTimelineMarker.h @@ -0,0 +1,46 @@ +/* -*- 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/. */ + +#ifndef mozilla_AutoTimelineMarker_h_ +#define mozilla_AutoTimelineMarker_h_ + +#include "mozilla/RefPtr.h" + +class nsIDocShell; + +namespace mozilla { + +// # AutoTimelineMarker +// +// An RAII class to trace some task in the platform by adding a start and end +// timeline marker pair. These markers are then rendered in the devtools' +// performance tool's waterfall graph. +// +// Example usage: +// +// { +// AutoTimelineMarker marker(mDocShell, "Parse CSS"); +// nsresult rv = ParseTheCSSFile(mFile); +// ... +// } +class MOZ_RAII AutoTimelineMarker { + // The name of the marker we are adding. + const char* mName; + + // The docshell that is associated with this marker. + RefPtr<nsIDocShell> mDocShell; + + public: + AutoTimelineMarker(nsIDocShell* aDocShell, const char* aName); + ~AutoTimelineMarker(); + + AutoTimelineMarker(const AutoTimelineMarker& aOther) = delete; + void operator=(const AutoTimelineMarker& aOther) = delete; +}; + +} // namespace mozilla + +#endif /* mozilla_AutoTimelineMarker_h_ */ diff --git a/docshell/base/timeline/CompositeTimelineMarker.h b/docshell/base/timeline/CompositeTimelineMarker.h new file mode 100644 index 0000000000..cd2896fefa --- /dev/null +++ b/docshell/base/timeline/CompositeTimelineMarker.h @@ -0,0 +1,31 @@ +/* -*- 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/. */ + +#ifndef mozilla_CompositeTimelineMarker_h_ +#define mozilla_CompositeTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class CompositeTimelineMarker : public TimelineMarker { + public: + CompositeTimelineMarker(const TimeStamp& aTime, + MarkerTracingType aTracingType) + : TimelineMarker("Composite", aTime, aTracingType) { + // Even though these markers end up being created on the main thread in the + // content or chrome processes, they actually trace down code in the + // compositor parent process. All the information for creating these markers + // is sent along via IPC to an nsView when a composite finishes. + // Mark this as 'off the main thread' to style it differently in frontends. + SetOffMainThread(true); + } +}; + +} // namespace mozilla + +#endif // mozilla_CompositeTimelineMarker_h_ diff --git a/docshell/base/timeline/ConsoleTimelineMarker.h b/docshell/base/timeline/ConsoleTimelineMarker.h new file mode 100644 index 0000000000..232aa1a60e --- /dev/null +++ b/docshell/base/timeline/ConsoleTimelineMarker.h @@ -0,0 +1,53 @@ +/* -*- 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/. */ + +#ifndef mozilla_ConsoleTimelineMarker_h_ +#define mozilla_ConsoleTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class ConsoleTimelineMarker : public TimelineMarker { + public: + ConsoleTimelineMarker(const nsAString& aCause, MarkerTracingType aTracingType) + : TimelineMarker("ConsoleTime", aTracingType), mCause(aCause) { + // Stack is captured by default on the "start" marker. Explicitly also + // capture stack on the "end" marker. + if (aTracingType == MarkerTracingType::END) { + CaptureStack(); + } + } + + virtual bool Equals(const AbstractTimelineMarker& aOther) override { + if (!TimelineMarker::Equals(aOther)) { + return false; + } + // Console markers must have matching causes as well. It is safe to perform + // a static_cast here as the previous equality check ensures that this is + // a console marker instance. + return mCause == static_cast<const ConsoleTimelineMarker*>(&aOther)->mCause; + } + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override { + TimelineMarker::AddDetails(aCx, aMarker); + + if (GetTracingType() == MarkerTracingType::START) { + aMarker.mCauseName.Construct(mCause); + } else { + aMarker.mEndStack = GetStack(); + } + } + + private: + nsString mCause; +}; + +} // namespace mozilla + +#endif // mozilla_ConsoleTimelineMarker_h_ diff --git a/docshell/base/timeline/DocLoadingTimelineMarker.h b/docshell/base/timeline/DocLoadingTimelineMarker.h new file mode 100644 index 0000000000..8f6a7db780 --- /dev/null +++ b/docshell/base/timeline/DocLoadingTimelineMarker.h @@ -0,0 +1,38 @@ +/* -*- 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/. */ + +#ifndef mozilla_DocLoadingTimelineMarker_h_ +#define mozilla_DocLoadingTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class DocLoadingTimelineMarker : public TimelineMarker { + public: + explicit DocLoadingTimelineMarker(const char* aName) + : TimelineMarker(aName, MarkerTracingType::TIMESTAMP), + mUnixTime(PR_Now()) {} + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override { + TimelineMarker::AddDetails(aCx, aMarker); + aMarker.mUnixTime.Construct(mUnixTime); + } + + private: + // Certain consumers might use Date.now() or similar for tracing time. + // However, TimelineMarkers use process creation as an epoch, which provides + // more precision. To allow syncing, attach an additional unix timestamp. + // Using this instead of `AbstractTimelineMarker::GetTime()'s` timestamp + // is strongly discouraged. + PRTime mUnixTime; +}; + +} // namespace mozilla + +#endif // mozilla_DocLoadingTimelineMarker_h_ diff --git a/docshell/base/timeline/EventTimelineMarker.h b/docshell/base/timeline/EventTimelineMarker.h new file mode 100644 index 0000000000..5e5bc5c4a6 --- /dev/null +++ b/docshell/base/timeline/EventTimelineMarker.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +#ifndef mozilla_EventTimelineMarker_h_ +#define mozilla_EventTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class EventTimelineMarker : public TimelineMarker { + public: + EventTimelineMarker(const nsAString& aType, uint16_t aPhase, + MarkerTracingType aTracingType) + : TimelineMarker("DOMEvent", aTracingType), + mType(aType), + mPhase(aPhase) {} + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override { + TimelineMarker::AddDetails(aCx, aMarker); + + if (GetTracingType() == MarkerTracingType::START) { + aMarker.mType.Construct(mType); + aMarker.mEventPhase.Construct(mPhase); + } + } + + private: + nsString mType; + uint16_t mPhase; +}; + +} // namespace mozilla + +#endif // mozilla_EventTimelineMarker_h_ diff --git a/docshell/base/timeline/JavascriptTimelineMarker.h b/docshell/base/timeline/JavascriptTimelineMarker.h new file mode 100644 index 0000000000..0e07f3b605 --- /dev/null +++ b/docshell/base/timeline/JavascriptTimelineMarker.h @@ -0,0 +1,96 @@ +/* -*- 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/. */ + +#ifndef mozilla_JavascriptTimelineMarker_h_ +#define mozilla_JavascriptTimelineMarker_h_ + +#include "TimelineMarker.h" + +#include "mozilla/Maybe.h" + +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ToJSValue.h" + +namespace mozilla { + +class JavascriptTimelineMarker : public TimelineMarker { + public: + // The caller owns |aAsyncCause| here, so we must copy it into a separate + // string for use later on. + JavascriptTimelineMarker(const char* aReason, const nsAString& aFunctionName, + const nsAString& aFileName, uint32_t aLineNumber, + MarkerTracingType aTracingType, + JS::Handle<JS::Value> aAsyncStack, + const char* aAsyncCause) + : TimelineMarker("Javascript", aTracingType, + MarkerStackRequest::NO_STACK), + mCause(NS_ConvertUTF8toUTF16(aReason)), + mFunctionName(aFunctionName), + mFileName(aFileName), + mLineNumber(aLineNumber), + mAsyncCause(aAsyncCause) { + JSContext* ctx = nsContentUtils::GetCurrentJSContext(); + if (ctx) { + mAsyncStack.init(ctx, aAsyncStack); + } + } + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override { + TimelineMarker::AddDetails(aCx, aMarker); + + aMarker.mCauseName.Construct(mCause); + + if (!mFunctionName.IsEmpty() || !mFileName.IsEmpty()) { + dom::RootedDictionary<dom::ProfileTimelineStackFrame> stackFrame(aCx); + stackFrame.mLine.Construct(mLineNumber); + stackFrame.mSource.Construct(mFileName); + stackFrame.mFunctionDisplayName.Construct(mFunctionName); + + if (mAsyncStack.isObject() && !mAsyncCause.IsEmpty()) { + JS::Rooted<JSObject*> asyncStack(aCx, &mAsyncStack.toObject()); + JS::Rooted<JSObject*> parentFrame(aCx); + JS::Rooted<JSString*> asyncCause( + aCx, JS_NewUCStringCopyN(aCx, mAsyncCause.BeginReading(), + mAsyncCause.Length())); + if (!asyncCause) { + JS_ClearPendingException(aCx); + return; + } + + if (JS::IsMaybeWrappedSavedFrame(asyncStack) && + !JS::CopyAsyncStack(aCx, asyncStack, asyncCause, &parentFrame, + mozilla::Nothing())) { + JS_ClearPendingException(aCx); + } else { + stackFrame.mAsyncParent = parentFrame; + } + } + + JS::Rooted<JS::Value> newStack(aCx); + if (ToJSValue(aCx, stackFrame, &newStack)) { + if (newStack.isObject()) { + aMarker.mStack = &newStack.toObject(); + } + } else { + JS_ClearPendingException(aCx); + } + } + } + + private: + nsString mCause; + nsString mFunctionName; + nsString mFileName; + uint32_t mLineNumber; + JS::PersistentRooted<JS::Value> mAsyncStack; + NS_ConvertUTF8toUTF16 mAsyncCause; +}; + +} // namespace mozilla + +#endif // mozilla_JavascriptTimelineMarker_h_ diff --git a/docshell/base/timeline/LayerTimelineMarker.h b/docshell/base/timeline/LayerTimelineMarker.h new file mode 100644 index 0000000000..7c7336ba52 --- /dev/null +++ b/docshell/base/timeline/LayerTimelineMarker.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef mozilla_LayerTimelineMarker_h_ +#define mozilla_LayerTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" +#include "mozilla/mozalloc_oom.h" +#include "nsRegion.h" + +namespace mozilla { + +class LayerTimelineMarker : public TimelineMarker { + public: + explicit LayerTimelineMarker(const nsIntRegion& aRegion) + : TimelineMarker("Layer", MarkerTracingType::HELPER_EVENT), + mRegion(aRegion) {} + + void AddLayerRectangles( + dom::Sequence<dom::ProfileTimelineLayerRect>& aRectangles) { + for (auto iter = mRegion.RectIter(); !iter.Done(); iter.Next()) { + const nsIntRect& iterRect = iter.Get(); + dom::ProfileTimelineLayerRect rect; + rect.mX = iterRect.X(); + rect.mY = iterRect.Y(); + rect.mWidth = iterRect.Width(); + rect.mHeight = iterRect.Height(); + if (!aRectangles.AppendElement(rect, fallible)) { + // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might + // involve multiple reallocations) and potentially crashing here, + // SetCapacity could be called outside the loop once. + mozalloc_handle_oom(0); + } + } + } + + private: + nsIntRegion mRegion; +}; + +} // namespace mozilla + +#endif // mozilla_LayerTimelineMarker_h_ diff --git a/docshell/base/timeline/MarkersStorage.h b/docshell/base/timeline/MarkersStorage.h new file mode 100644 index 0000000000..3ee7623068 --- /dev/null +++ b/docshell/base/timeline/MarkersStorage.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +#ifndef mozilla_MarkersStorage_h_ +#define mozilla_MarkersStorage_h_ + +#include "TimelineMarkerEnums.h" // for MarkerReleaseRequest +#include "MainThreadUtils.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/LinkedList.h" +#include "nsTArray.h" + +namespace mozilla { +class AbstractTimelineMarker; + +namespace dom { +struct ProfileTimelineMarker; +} + +class MarkersStorage : public LinkedListElement<MarkersStorage> { + public: + MarkersStorage() { MOZ_ASSERT(NS_IsMainThread()); } + virtual ~MarkersStorage() { MOZ_ASSERT(NS_IsMainThread()); } + + MarkersStorage(const MarkersStorage& aOther) = delete; + void operator=(const MarkersStorage& aOther) = delete; + + virtual void AddMarker(UniquePtr<AbstractTimelineMarker>&& aMarker) = 0; + virtual void AddOTMTMarker(UniquePtr<AbstractTimelineMarker>&& aMarker) = 0; + virtual void ClearMarkers() = 0; + virtual void PopMarkers(JSContext* aCx, + nsTArray<dom::ProfileTimelineMarker>& aStore) = 0; +}; + +} // namespace mozilla + +#endif /* mozilla_MarkersStorage_h_ */ diff --git a/docshell/base/timeline/MessagePortTimelineMarker.h b/docshell/base/timeline/MessagePortTimelineMarker.h new file mode 100644 index 0000000000..c6d91fa7eb --- /dev/null +++ b/docshell/base/timeline/MessagePortTimelineMarker.h @@ -0,0 +1,46 @@ +/* -*- 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/. */ + +#ifndef mozilla_MessagePortTimelineMarker_h_ +#define mozilla_MessagePortTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class MessagePortTimelineMarker : public TimelineMarker { + public: + MessagePortTimelineMarker( + dom::ProfileTimelineMessagePortOperationType aOperationType, + MarkerTracingType aTracingType) + : TimelineMarker("MessagePort", aTracingType, + MarkerStackRequest::NO_STACK), + mOperationType(aOperationType) {} + + virtual UniquePtr<AbstractTimelineMarker> Clone() override { + MessagePortTimelineMarker* clone = + new MessagePortTimelineMarker(mOperationType, GetTracingType()); + clone->SetCustomTime(GetTime()); + return UniquePtr<AbstractTimelineMarker>(clone); + } + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override { + TimelineMarker::AddDetails(aCx, aMarker); + + if (GetTracingType() == MarkerTracingType::START) { + aMarker.mMessagePortOperation.Construct(mOperationType); + } + } + + private: + dom::ProfileTimelineMessagePortOperationType mOperationType; +}; + +} // namespace mozilla + +#endif /* mozilla_MessagePortTimelineMarker_h_ */ diff --git a/docshell/base/timeline/ObservedDocShell.cpp b/docshell/base/timeline/ObservedDocShell.cpp new file mode 100644 index 0000000000..e4a2b319f5 --- /dev/null +++ b/docshell/base/timeline/ObservedDocShell.cpp @@ -0,0 +1,171 @@ +/* -*- 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/. */ + +#include "ObservedDocShell.h" + +#include <utility> + +#include "AbstractTimelineMarker.h" +#include "LayerTimelineMarker.h" +#include "MainThreadUtils.h" +#include "mozilla/AutoRestore.h" +#include "nsIDocShell.h" + +namespace mozilla { + +ObservedDocShell::ObservedDocShell(nsIDocShell* aDocShell) + : mDocShell(aDocShell), mLock("ObservedDocShellMutex") { + MOZ_ASSERT(NS_IsMainThread()); +} + +void ObservedDocShell::AddMarker(UniquePtr<AbstractTimelineMarker>&& aMarker) { + // Only allow main thread markers to go into this list. No need to lock + // here since `mTimelineMarkers` will only be accessed or modified on the + // main thread only. + MOZ_ASSERT(NS_IsMainThread()); + // Don't accept any markers generated by the process of popping + // markers. + if (!mPopping) { + mTimelineMarkers.AppendElement(std::move(aMarker)); + } +} + +void ObservedDocShell::AddOTMTMarker( + UniquePtr<AbstractTimelineMarker>&& aMarker) { + // Only allow off the main thread markers to go into this list. Since most + // of our markers come from the main thread, be a little more efficient and + // avoid dealing with multithreading scenarios until all the markers are + // actually cleared or popped in `ClearMarkers` or `PopMarkers`. + MOZ_ASSERT(!NS_IsMainThread()); + MutexAutoLock lock(mLock); // for `mOffTheMainThreadTimelineMarkers`. + mOffTheMainThreadTimelineMarkers.AppendElement(std::move(aMarker)); +} + +void ObservedDocShell::ClearMarkers() { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mLock); // for `mOffTheMainThreadTimelineMarkers`. + mTimelineMarkers.Clear(); + mOffTheMainThreadTimelineMarkers.Clear(); +} + +void ObservedDocShell::PopMarkers( + JSContext* aCx, nsTArray<dom::ProfileTimelineMarker>& aStore) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_RELEASE_ASSERT(!mPopping); + AutoRestore<bool> resetPopping(mPopping); + mPopping = true; + + { + MutexAutoLock lock(mLock); // for `mOffTheMainThreadTimelineMarkers`. + + // First, move all of our markers into a single array. We'll chose + // the `mTimelineMarkers` store because that's where we expect most of + // our markers to be, and we can access it without holding the lock. + mTimelineMarkers.AppendElements( + std::move(mOffTheMainThreadTimelineMarkers)); + } + + // If we see an unpaired START, we keep it around for the next call + // to ObservedDocShell::PopMarkers. We store the kept START objects here. + nsTArray<UniquePtr<AbstractTimelineMarker>> keptStartMarkers; + + for (uint32_t i = 0; i < mTimelineMarkers.Length(); ++i) { + UniquePtr<AbstractTimelineMarker>& startPayload = + mTimelineMarkers.ElementAt(i); + + // If this is a TIMESTAMP marker, there's no corresponding END, + // as it's a single unit of time, not a duration. + if (startPayload->GetTracingType() == MarkerTracingType::TIMESTAMP) { + dom::ProfileTimelineMarker* marker = aStore.AppendElement(); + marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); + marker->mStart = startPayload->GetTime(); + marker->mEnd = startPayload->GetTime(); + marker->mStack = startPayload->GetStack(); + startPayload->AddDetails(aCx, *marker); + continue; + } + + // Whenever a START marker is found, look for the corresponding END + // and build a {name,start,end} JS object. + if (startPayload->GetTracingType() == MarkerTracingType::START) { + bool hasSeenEnd = false; + + // "Paint" markers are different because painting is handled at root + // docshell level. The information that a paint was done is stored at + // sub-docshell level, but we can only be sure that a paint did actually + // happen in if a "Layer" marker was recorded too. + bool startIsPaintType = strcmp(startPayload->GetName(), "Paint") == 0; + bool hasSeenLayerType = false; + + // If we are processing a "Paint" marker, we append information from + // all the embedded "Layer" markers to this array. + dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles; + + // DOM events can be nested, so we must take care when searching + // for the matching end. It doesn't hurt to apply this logic to + // all event types. + uint32_t markerDepth = 0; + + // The assumption is that the devtools timeline flushes markers frequently + // enough for the amount of markers to always be small enough that the + // nested for loop isn't going to be a performance problem. + for (uint32_t j = i + 1; j < mTimelineMarkers.Length(); ++j) { + UniquePtr<AbstractTimelineMarker>& endPayload = + mTimelineMarkers.ElementAt(j); + bool endIsLayerType = strcmp(endPayload->GetName(), "Layer") == 0; + + // Look for "Layer" markers to stream out "Paint" markers. + if (startIsPaintType && endIsLayerType) { + AbstractTimelineMarker* raw = endPayload.get(); + LayerTimelineMarker* layerPayload = + static_cast<LayerTimelineMarker*>(raw); + layerPayload->AddLayerRectangles(layerRectangles); + hasSeenLayerType = true; + } + if (!startPayload->Equals(*endPayload)) { + continue; + } + if (endPayload->GetTracingType() == MarkerTracingType::START) { + ++markerDepth; + continue; + } + if (endPayload->GetTracingType() == MarkerTracingType::END) { + if (markerDepth > 0) { + --markerDepth; + continue; + } + if (!startIsPaintType || (startIsPaintType && hasSeenLayerType)) { + dom::ProfileTimelineMarker* marker = aStore.AppendElement(); + marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); + marker->mStart = startPayload->GetTime(); + marker->mEnd = endPayload->GetTime(); + marker->mStack = startPayload->GetStack(); + if (hasSeenLayerType) { + marker->mRectangles.Construct(layerRectangles); + } + startPayload->AddDetails(aCx, *marker); + endPayload->AddDetails(aCx, *marker); + } + hasSeenEnd = true; + break; + } + } + + // If we did not see the corresponding END, keep the START. + if (!hasSeenEnd) { + keptStartMarkers.AppendElement( + std::move(mTimelineMarkers.ElementAt(i))); + mTimelineMarkers.RemoveElementAt(i); + --i; + } + } + } + + mTimelineMarkers = std::move(keptStartMarkers); +} + +} // namespace mozilla diff --git a/docshell/base/timeline/ObservedDocShell.h b/docshell/base/timeline/ObservedDocShell.h new file mode 100644 index 0000000000..9d0c39fcb2 --- /dev/null +++ b/docshell/base/timeline/ObservedDocShell.h @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +#ifndef mozilla_ObservedDocShell_h_ +#define mozilla_ObservedDocShell_h_ + +#include "MarkersStorage.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Mutex.h" +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" + +class nsIDocShell; + +namespace mozilla { +class AbstractTimelineMarker; + +namespace dom { +struct ProfileTimelineMarker; +} + +// # ObservedDocShell +// +// A wrapper around a docshell for which docshell-specific markers are +// allowed to exist. See TimelineConsumers for register/unregister logic. +class ObservedDocShell : public MarkersStorage { + private: + RefPtr<nsIDocShell> mDocShell; + + // Main thread only. + nsTArray<UniquePtr<AbstractTimelineMarker>> mTimelineMarkers; + bool mPopping = false; + + // Off the main thread only. + Mutex mLock; + nsTArray<UniquePtr<AbstractTimelineMarker>> mOffTheMainThreadTimelineMarkers + MOZ_GUARDED_BY(mLock); + + public: + explicit ObservedDocShell(nsIDocShell* aDocShell); + nsIDocShell* operator*() const { return mDocShell.get(); } + + void AddMarker(UniquePtr<AbstractTimelineMarker>&& aMarker) override; + void AddOTMTMarker(UniquePtr<AbstractTimelineMarker>&& aMarker) override; + void ClearMarkers() override; + void PopMarkers(JSContext* aCx, + nsTArray<dom::ProfileTimelineMarker>& aStore) override; +}; + +} // namespace mozilla + +#endif /* mozilla_ObservedDocShell_h_ */ diff --git a/docshell/base/timeline/RestyleTimelineMarker.h b/docshell/base/timeline/RestyleTimelineMarker.h new file mode 100644 index 0000000000..4cce10d94b --- /dev/null +++ b/docshell/base/timeline/RestyleTimelineMarker.h @@ -0,0 +1,37 @@ +/* -*- 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/. */ + +#ifndef mozilla_RestyleTimelineMarker_h_ +#define mozilla_RestyleTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class RestyleTimelineMarker : public TimelineMarker { + public: + RestyleTimelineMarker(bool aIsAnimationOnly, MarkerTracingType aTracingType) + : TimelineMarker("Styles", aTracingType) { + mIsAnimationOnly = aIsAnimationOnly; + } + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override { + TimelineMarker::AddDetails(aCx, aMarker); + + if (GetTracingType() == MarkerTracingType::START) { + aMarker.mIsAnimationOnly.Construct(mIsAnimationOnly); + } + } + + private: + bool mIsAnimationOnly; +}; + +} // namespace mozilla + +#endif // mozilla_RestyleTimelineMarker_h_ diff --git a/docshell/base/timeline/TimelineConsumers.cpp b/docshell/base/timeline/TimelineConsumers.cpp new file mode 100644 index 0000000000..e475b74ee1 --- /dev/null +++ b/docshell/base/timeline/TimelineConsumers.cpp @@ -0,0 +1,202 @@ +/* -*- 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/. */ + +#include "TimelineConsumers.h" + +#include "mozilla/ObservedDocShell.h" +#include "mozilla/TimelineMarker.h" +#include "jsapi.h" +#include "nsAppRunner.h" // for XRE_IsContentProcess, XRE_IsParentProcess +#include "nsCRT.h" +#include "nsDocShell.h" + +namespace mozilla { + +StaticMutex TimelineConsumers::sMutex; + +uint32_t TimelineConsumers::sActiveConsumers = 0; + +StaticAutoPtr<LinkedList<MarkersStorage>> TimelineConsumers::sMarkersStores; + +LinkedList<MarkersStorage>& TimelineConsumers::MarkersStores() { + if (!sMarkersStores) { + sMarkersStores = new LinkedList<MarkersStorage>; + } + return *sMarkersStores; +} + +void TimelineConsumers::AddConsumer(nsDocShell* aDocShell) { + MOZ_ASSERT(NS_IsMainThread()); + StaticMutexAutoLock lock( + sMutex); // for `sActiveConsumers` and `sMarkersStores`. + + UniquePtr<ObservedDocShell>& observed = aDocShell->mObserved; + MOZ_ASSERT(!observed); + + if (sActiveConsumers == 0) { + JS::SetProfileTimelineRecordingEnabled(true); + } + sActiveConsumers++; + + ObservedDocShell* obsDocShell = new ObservedDocShell(aDocShell); + MarkersStorage* storage = static_cast<MarkersStorage*>(obsDocShell); + + observed.reset(obsDocShell); + MarkersStores().insertFront(storage); +} + +void TimelineConsumers::RemoveConsumer(nsDocShell* aDocShell) { + MOZ_ASSERT(NS_IsMainThread()); + StaticMutexAutoLock lock( + sMutex); // for `sActiveConsumers` and `sMarkersStores`. + + UniquePtr<ObservedDocShell>& observed = aDocShell->mObserved; + MOZ_ASSERT(observed); + + sActiveConsumers--; + if (sActiveConsumers == 0) { + JS::SetProfileTimelineRecordingEnabled(false); + } + + // Clear all markers from the `mTimelineMarkers` store. + observed->ClearMarkers(); + // Remove self from the `sMarkersStores` store. + observed->remove(); + // Prepare for becoming a consumer later. + observed.reset(nullptr); +} + +bool TimelineConsumers::HasConsumer(nsIDocShell* aDocShell) { + MOZ_ASSERT(NS_IsMainThread()); + return aDocShell ? aDocShell->GetRecordProfileTimelineMarkers() : false; +} + +bool TimelineConsumers::IsEmpty() { + StaticMutexAutoLock lock(sMutex); // for `sActiveConsumers`. + return sActiveConsumers == 0; +} + +void TimelineConsumers::AddMarkerForDocShell(nsDocShell* aDocShell, + const char* aName, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest) { + MOZ_ASSERT(NS_IsMainThread()); + if (HasConsumer(aDocShell)) { + aDocShell->mObserved->AddMarker( + MakeUnique<TimelineMarker>(aName, aTracingType, aStackRequest)); + } +} + +void TimelineConsumers::AddMarkerForDocShell(nsDocShell* aDocShell, + const char* aName, + const TimeStamp& aTime, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest) { + MOZ_ASSERT(NS_IsMainThread()); + if (HasConsumer(aDocShell)) { + aDocShell->mObserved->AddMarker( + MakeUnique<TimelineMarker>(aName, aTime, aTracingType, aStackRequest)); + } +} + +void TimelineConsumers::AddMarkerForDocShell( + nsDocShell* aDocShell, UniquePtr<AbstractTimelineMarker>&& aMarker) { + MOZ_ASSERT(NS_IsMainThread()); + if (HasConsumer(aDocShell)) { + aDocShell->mObserved->AddMarker(std::move(aMarker)); + } +} + +void TimelineConsumers::AddMarkerForDocShell(nsIDocShell* aDocShell, + const char* aName, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest) { + MOZ_ASSERT(NS_IsMainThread()); + AddMarkerForDocShell(static_cast<nsDocShell*>(aDocShell), aName, aTracingType, + aStackRequest); +} + +void TimelineConsumers::AddMarkerForDocShell(nsIDocShell* aDocShell, + const char* aName, + const TimeStamp& aTime, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest) { + MOZ_ASSERT(NS_IsMainThread()); + AddMarkerForDocShell(static_cast<nsDocShell*>(aDocShell), aName, aTime, + aTracingType, aStackRequest); +} + +void TimelineConsumers::AddMarkerForDocShell( + nsIDocShell* aDocShell, UniquePtr<AbstractTimelineMarker>&& aMarker) { + MOZ_ASSERT(NS_IsMainThread()); + AddMarkerForDocShell(static_cast<nsDocShell*>(aDocShell), std::move(aMarker)); +} + +void TimelineConsumers::AddMarkerForAllObservedDocShells( + const char* aName, MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest /* = STACK */) { + bool isMainThread = NS_IsMainThread(); + StaticMutexAutoLock lock(sMutex); // for `sMarkersStores`. + + for (MarkersStorage* storage = MarkersStores().getFirst(); storage != nullptr; + storage = storage->getNext()) { + UniquePtr<AbstractTimelineMarker> marker = + MakeUnique<TimelineMarker>(aName, aTracingType, aStackRequest); + if (isMainThread) { + storage->AddMarker(std::move(marker)); + } else { + storage->AddOTMTMarker(std::move(marker)); + } + } +} + +void TimelineConsumers::AddMarkerForAllObservedDocShells( + const char* aName, const TimeStamp& aTime, MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest /* = STACK */) { + bool isMainThread = NS_IsMainThread(); + StaticMutexAutoLock lock(sMutex); // for `sMarkersStores`. + + for (MarkersStorage* storage = MarkersStores().getFirst(); storage != nullptr; + storage = storage->getNext()) { + UniquePtr<AbstractTimelineMarker> marker = + MakeUnique<TimelineMarker>(aName, aTime, aTracingType, aStackRequest); + if (isMainThread) { + storage->AddMarker(std::move(marker)); + } else { + storage->AddOTMTMarker(std::move(marker)); + } + } +} + +void TimelineConsumers::AddMarkerForAllObservedDocShells( + UniquePtr<AbstractTimelineMarker>& aMarker) { + bool isMainThread = NS_IsMainThread(); + StaticMutexAutoLock lock(sMutex); // for `sMarkersStores`. + + for (MarkersStorage* storage = MarkersStores().getFirst(); storage != nullptr; + storage = storage->getNext()) { + UniquePtr<AbstractTimelineMarker> clone = aMarker->Clone(); + if (isMainThread) { + storage->AddMarker(std::move(clone)); + } else { + storage->AddOTMTMarker(std::move(clone)); + } + } +} + +void TimelineConsumers::PopMarkers( + nsDocShell* aDocShell, JSContext* aCx, + nsTArray<dom::ProfileTimelineMarker>& aStore) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aDocShell || !aDocShell->mObserved) { + return; + } + + aDocShell->mObserved->PopMarkers(aCx, aStore); +} + +} // namespace mozilla diff --git a/docshell/base/timeline/TimelineConsumers.h b/docshell/base/timeline/TimelineConsumers.h new file mode 100644 index 0000000000..a199560d8c --- /dev/null +++ b/docshell/base/timeline/TimelineConsumers.h @@ -0,0 +1,113 @@ +/* -*- 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/. */ + +#ifndef mozilla_TimelineConsumers_h_ +#define mozilla_TimelineConsumers_h_ + +#include "nsIObserver.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/LinkedList.h" +#include "mozilla/StaticMutex.h" +#include "nsTArray.h" +#include "TimelineMarkerEnums.h" // for MarkerTracingType + +class nsDocShell; +class nsIDocShell; +struct JSContext; + +namespace mozilla { +class TimeStamp; +class MarkersStorage; +class AbstractTimelineMarker; + +namespace dom { +struct ProfileTimelineMarker; +} + +class TimelineConsumers { + public: + // Methods for registering interested consumers (i.e. "devtools toolboxes"). + // Each consumer should be directly focused on a particular docshell, but + // timeline markers don't necessarily have to be tied to that docshell. + // See the public `AddMarker*` methods below. + // Main thread only. + static void AddConsumer(nsDocShell* aDocShell); + static void RemoveConsumer(nsDocShell* aDocShell); + + static bool HasConsumer(nsIDocShell* aDocShell); + + // Checks if there's any existing interested consumer. + // May be called from any thread. + static bool IsEmpty(); + + // Methods for adding markers relevant for particular docshells, or generic + // (meaning that they either can't be tied to a particular docshell, or one + // wasn't accessible in the part of the codebase where they're instantiated). + // These will only add markers if at least one docshell is currently being + // observed by a timeline. Markers tied to a particular docshell won't be + // created unless that docshell is specifically being currently observed. + // See nsIDocShell::recordProfileTimelineMarkers + + // These methods create a basic TimelineMarker from a name and some metadata, + // relevant for a specific docshell. + // Main thread only. + static void AddMarkerForDocShell( + nsDocShell* aDocShell, const char* aName, MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + static void AddMarkerForDocShell( + nsIDocShell* aDocShell, const char* aName, MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + + static void AddMarkerForDocShell( + nsDocShell* aDocShell, const char* aName, const TimeStamp& aTime, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + static void AddMarkerForDocShell( + nsIDocShell* aDocShell, const char* aName, const TimeStamp& aTime, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + + // These methods register and receive ownership of an already created marker, + // relevant for a specific docshell. + // Main thread only. + static void AddMarkerForDocShell(nsDocShell* aDocShell, + UniquePtr<AbstractTimelineMarker>&& aMarker); + static void AddMarkerForDocShell(nsIDocShell* aDocShell, + UniquePtr<AbstractTimelineMarker>&& aMarker); + + // These methods create a basic marker from a name and some metadata, + // which doesn't have to be relevant to a specific docshell. + // May be called from any thread. + static void AddMarkerForAllObservedDocShells( + const char* aName, MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + static void AddMarkerForAllObservedDocShells( + const char* aName, const TimeStamp& aTime, MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + + // This method clones and registers an already instantiated marker, + // which doesn't have to be relevant to a specific docshell. + // May be called from any thread. + static void AddMarkerForAllObservedDocShells( + UniquePtr<AbstractTimelineMarker>& aMarker); + + static void PopMarkers(nsDocShell* aDocShell, JSContext* aCx, + nsTArray<dom::ProfileTimelineMarker>& aStore); + + private: + static StaticMutex sMutex; + + static LinkedList<MarkersStorage>& MarkersStores() MOZ_REQUIRES(sMutex); + + static uint32_t sActiveConsumers MOZ_GUARDED_BY(sMutex); + static StaticAutoPtr<LinkedList<MarkersStorage>> sMarkersStores + MOZ_GUARDED_BY(sMutex); +}; + +} // namespace mozilla + +#endif /* mozilla_TimelineConsumers_h_ */ diff --git a/docshell/base/timeline/TimelineMarker.cpp b/docshell/base/timeline/TimelineMarker.cpp new file mode 100644 index 0000000000..9f09ebf0a5 --- /dev/null +++ b/docshell/base/timeline/TimelineMarker.cpp @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +#include "TimelineMarker.h" + +#include "jsapi.h" +#include "js/Exception.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" +#include "nsContentUtils.h" + +namespace mozilla { + +TimelineMarker::TimelineMarker(const char* aName, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest) + : AbstractTimelineMarker(aName, aTracingType) { + CaptureStackIfNecessary(aTracingType, aStackRequest); +} + +TimelineMarker::TimelineMarker(const char* aName, const TimeStamp& aTime, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest) + : AbstractTimelineMarker(aName, aTime, aTracingType) { + CaptureStackIfNecessary(aTracingType, aStackRequest); +} + +void TimelineMarker::AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) { + if (GetTracingType() == MarkerTracingType::START) { + aMarker.mProcessType.Construct(GetProcessType()); + aMarker.mIsOffMainThread.Construct(IsOffMainThread()); + } +} + +JSObject* TimelineMarker::GetStack() { + if (mStackTrace.initialized()) { + return mStackTrace; + } + return nullptr; +} + +void TimelineMarker::CaptureStack() { + JSContext* ctx = nsContentUtils::GetCurrentJSContext(); + if (ctx) { + JS::Rooted<JSObject*> stack(ctx); + if (JS::CaptureCurrentStack(ctx, &stack)) { + mStackTrace.init(ctx, stack.get()); + } else { + JS_ClearPendingException(ctx); + } + } +} + +void TimelineMarker::CaptureStackIfNecessary(MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest) { + if ((aTracingType == MarkerTracingType::START || + aTracingType == MarkerTracingType::TIMESTAMP) && + aStackRequest != MarkerStackRequest::NO_STACK) { + CaptureStack(); + } +} + +} // namespace mozilla diff --git a/docshell/base/timeline/TimelineMarker.h b/docshell/base/timeline/TimelineMarker.h new file mode 100644 index 0000000000..b7c5a83ce9 --- /dev/null +++ b/docshell/base/timeline/TimelineMarker.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef mozilla_TimelineMarker_h_ +#define mozilla_TimelineMarker_h_ + +#include "AbstractTimelineMarker.h" +#include "js/RootingAPI.h" + +namespace mozilla { + +// Objects of this type can be added to the timeline if there is an interested +// consumer. The class can also be subclassed to let a given marker creator +// provide custom details. +class TimelineMarker : public AbstractTimelineMarker { + public: + TimelineMarker(const char* aName, MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + + TimelineMarker(const char* aName, const TimeStamp& aTime, + MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest = MarkerStackRequest::STACK); + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override; + virtual JSObject* GetStack() override; + + protected: + void CaptureStack(); + + private: + // While normally it is not a good idea to make a persistent root, + // in this case changing nsDocShell to participate in cycle + // collection was deemed too invasive, and the markers are only held + // here temporarily to boot. + JS::PersistentRooted<JSObject*> mStackTrace; + + void CaptureStackIfNecessary(MarkerTracingType aTracingType, + MarkerStackRequest aStackRequest); +}; + +} // namespace mozilla + +#endif /* mozilla_TimelineMarker_h_ */ diff --git a/docshell/base/timeline/TimelineMarkerEnums.h b/docshell/base/timeline/TimelineMarkerEnums.h new file mode 100644 index 0000000000..6c6a39245c --- /dev/null +++ b/docshell/base/timeline/TimelineMarkerEnums.h @@ -0,0 +1,18 @@ +/* -*- 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/. */ + +#ifndef mozilla_TimelineMarkerEnums_h_ +#define mozilla_TimelineMarkerEnums_h_ + +namespace mozilla { + +enum class MarkerTracingType { START, END, TIMESTAMP, HELPER_EVENT }; + +enum class MarkerStackRequest { STACK, NO_STACK }; + +} // namespace mozilla + +#endif // mozilla_TimelineMarkerEnums_h_ diff --git a/docshell/base/timeline/TimestampTimelineMarker.h b/docshell/base/timeline/TimestampTimelineMarker.h new file mode 100644 index 0000000000..6950f85899 --- /dev/null +++ b/docshell/base/timeline/TimestampTimelineMarker.h @@ -0,0 +1,36 @@ +/* -*- 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/. */ + +#ifndef mozilla_TimestampTimelineMarker_h_ +#define mozilla_TimestampTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class TimestampTimelineMarker : public TimelineMarker { + public: + explicit TimestampTimelineMarker(const nsAString& aCause) + : TimelineMarker("TimeStamp", MarkerTracingType::TIMESTAMP), + mCause(aCause) {} + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override { + TimelineMarker::AddDetails(aCx, aMarker); + + if (!mCause.IsEmpty()) { + aMarker.mCauseName.Construct(mCause); + } + } + + private: + nsString mCause; +}; + +} // namespace mozilla + +#endif // mozilla_TimestampTimelineMarker_h_ diff --git a/docshell/base/timeline/WorkerTimelineMarker.h b/docshell/base/timeline/WorkerTimelineMarker.h new file mode 100644 index 0000000000..c0c0517b09 --- /dev/null +++ b/docshell/base/timeline/WorkerTimelineMarker.h @@ -0,0 +1,44 @@ +/* -*- 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/. */ + +#ifndef mozilla_WorkerTimelineMarker_h_ +#define mozilla_WorkerTimelineMarker_h_ + +#include "TimelineMarker.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" + +namespace mozilla { + +class WorkerTimelineMarker : public TimelineMarker { + public: + WorkerTimelineMarker(dom::ProfileTimelineWorkerOperationType aOperationType, + MarkerTracingType aTracingType) + : TimelineMarker("Worker", aTracingType, MarkerStackRequest::NO_STACK), + mOperationType(aOperationType) {} + + virtual UniquePtr<AbstractTimelineMarker> Clone() override { + WorkerTimelineMarker* clone = + new WorkerTimelineMarker(mOperationType, GetTracingType()); + clone->SetCustomTime(GetTime()); + return UniquePtr<AbstractTimelineMarker>(clone); + } + + virtual void AddDetails(JSContext* aCx, + dom::ProfileTimelineMarker& aMarker) override { + TimelineMarker::AddDetails(aCx, aMarker); + + if (GetTracingType() == MarkerTracingType::START) { + aMarker.mWorkerOperation.Construct(mOperationType); + } + } + + private: + dom::ProfileTimelineWorkerOperationType mOperationType; +}; + +} // namespace mozilla + +#endif /* mozilla_WorkerTimelineMarker_h_ */ diff --git a/docshell/base/timeline/moz.build b/docshell/base/timeline/moz.build new file mode 100644 index 0000000000..d5daf84ae7 --- /dev/null +++ b/docshell/base/timeline/moz.build @@ -0,0 +1,44 @@ +# -*- 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 = ("DevTools", "Performance Tools (Profiler/Timeline)") + +EXPORTS.mozilla += [ + "AbstractTimelineMarker.h", + "AutoGlobalTimelineMarker.h", + "AutoRestyleTimelineMarker.h", + "AutoTimelineMarker.h", + "CompositeTimelineMarker.h", + "ConsoleTimelineMarker.h", + "DocLoadingTimelineMarker.h", + "EventTimelineMarker.h", + "JavascriptTimelineMarker.h", + "LayerTimelineMarker.h", + "MarkersStorage.h", + "MessagePortTimelineMarker.h", + "ObservedDocShell.h", + "RestyleTimelineMarker.h", + "TimelineConsumers.h", + "TimelineMarker.h", + "TimelineMarkerEnums.h", + "TimestampTimelineMarker.h", + "WorkerTimelineMarker.h", +] + +UNIFIED_SOURCES += [ + "AbstractTimelineMarker.cpp", + "AutoGlobalTimelineMarker.cpp", + "AutoRestyleTimelineMarker.cpp", + "AutoTimelineMarker.cpp", + "ObservedDocShell.cpp", + "TimelineConsumers.cpp", + "TimelineMarker.cpp", +] + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += ["/docshell/base"] diff --git a/docshell/base/timeline/readme.md b/docshell/base/timeline/readme.md new file mode 100644 index 0000000000..d2c3d5c215 --- /dev/null +++ b/docshell/base/timeline/readme.md @@ -0,0 +1,97 @@ + +#Timeline + +The files in this directory are concerned with providing the backend platform features required for the developer tools interested in tracking down operations done in Gecko. The mechanism we use to define these operations are `markers`. + +Examples of traced operations include: + +* Style Recalculation +* Layout +* Painting +* JavaScript run-to-completion +* HTML parsing +* etc. + +The traced operations are displayed in the DevTools Performance tool's timeline. + +This is an overview of how everything works and can be extended. + +##MarkersStorage +A `MarkersStorage` is an abstract class defining a place where timeline markers may be held. It defines an interface with pure virtual functions to highlight how this storage can be interacted with: + +- `AddMarker`: adding a marker, from the main thread only +- `AddOTMTMarker`: adding a marker off the main thread only +- `ClearMarkers`: clearing all accumulated markers (both from the main thread and off it) +- `PopMarkers`: popping all accumulated markers (both from the main thread and off it). + +Note on why we handle on/off the main thread markers separately: since most of our markers will come from the main thread, we can be a little more efficient and avoid dealing with multithreading scenarios until all the markers are actually cleared or popped in `ClearMarkers` or `PopMarkers`. Main thread markers may only be added via `AddMarker`, while off the main thread markers may only be added via `AddOTMTMarker`. Clearing and popping markers will yield until all operations involving off the main thread markers finish. When popping, the markers accumulated off the main thread will be moved over. We expect popping to be fairly infrequent (every few hundred milliseconds, currently we schedule this to happen every 200ms). + +##ObservedDocShell +The only implementation of a MarkersStorage we have right now is an `ObservedDocShell`. + +Instances of `ObservedDocShell` accumulate markers that are *mostly* about a particular docshell. At a high level, for example, an `ObservedDocshell` would be created when a timeline tool is opened on a page. It is reasonable to assume that most operations which are interesting for that particular page happen on the main thread. However certain operations may happen outside of it, yet are interesting for its developers, for which markers can be created as well (e.g. web audio stuff, service workers etc.). It is also reasonable to assume that a docshell may sometimes not be easily accessible from certain parts of the platform code, but for which markers still need to be created. + +Therefore, the following scenarios arise: + +- a). creating a marker on the main thread about a particular docshell + +- b). creating a marker on the main thread without pinpointing to an affected docshell (unlikely, but allowed; in this case such a marker would have to be stored in all currently existing `ObservedDocShell` instances) + +- c). creating a marker off the main thread about a particular docshell (impossible; docshells can't be referenced outside the main thread, in which case some other type of identification mechanism needs to be put in place). + +- d). creating a marker off the main thread without pinpointing to a particular docshell (same path as c. here, such a marker would have to be stored in all currently existing `ObservedDocShell` instances). + +An observed docshell (in other words, "a docshell for which a timeline tool was opened") can thus receive both main thread and off the main thread markers. + +Cross-process markers are unnecessary at the moment, but tracked in bug 1200120. + +##TimelineConsumers +A `TimelineConsumer` is a singleton that facilitates access to `ObservedDocShell` instances. This is where a docshell can register/unregister itself as being observed via the `AddConsumer` and `RemoveConsumer` methods. + +All markers may only be stored via this singleton. Certain helper methods are available: + +* Main thread only +`AddMarkerForDocShell(nsDocShell*, const char*, MarkerTracingType)` +`AddMarkerForDocShell(nsDocShell*, const char*, const TimeStamp&, MarkerTracingType)` +`AddMarkerForDocShell(nsDocShell*, UniquePtr<AbstractTimelineMarker>&&)` + +* Any thread +`AddMarkerForAllObservedDocShells(const char*, MarkerTracingType)` +`AddMarkerForAllObservedDocShells(const char*, const TimeStamp&, MarkerTracingType)` +`AddMarkerForAllObservedDocShells(UniquePtr<AbstractTimelineMarker>&)` + +The "main thread only" methods deal with point a). described above. The "any thread" methods deal with points b). and d). + +##AbstractTimelineMarker + +All markers inherit from this abstract class, providing a simple thread-safe extendable blueprint. + +Markers are readonly after instantiation, and will always be identified by a name, a timestamp and their tracing type (`START`, `END`, `TIMESTAMP`). It *should not* make sense to modify their data after their creation. + +There are only two accessible constructors: +`AbstractTimelineMarker(const char*, MarkerTracingType)` +`AbstractTimelineMarker(const char*, const TimeStamp&, MarkerTracingType)` +which create a marker with a name and a tracing type. If unspecified, the corresponding timestamp will be the current instantiation time. Instantiating a marker *much later* after a particular operation is possible, but be careful providing the correct timestamp. + +The `AddDetails` virtual method should be implemented by subclasses when creating WebIDL versions of these markers, which will be sent over to a JavaScript frontend. + +##TimelineMarker +A `TimelineMarker` is the main `AbstractTimelineMarker` implementation. They allow attaching a JavaScript stack on `START` and `TIMESTAMP` markers. + +These markers will be created when using the `TimelineConsumers` helper methods which take in a string, a tracing type and (optionally) a timestamp. For more complex markers, subclasses are encouraged. See `EventTimelineMarker` or `ConsoleTimelineMarker` for some examples. + +##RAII + +### mozilla::AutoTimelineMarker + +The easiest way to trace Gecko events/tasks with start and end timeline markers is to use the `mozilla::AutoTimelineMarker` RAII class. It automatically adds the start marker on construction, and adds the end marker on destruction. Don't worry too much about potential performance impact! It only actually adds the markers when the given docshell is being observed by a timeline consumer, so essentially nothing will happen if a tool to inspect those markers isn't specifically open. + +This class may only be used on the main thread, and pointer to a docshell is necessary. If the docshell is a nullptr, nothing happens and this operation fails silently. + +Example: `AutoTimelineMarker marker(aTargetNode->OwnerDoc()->GetDocShell(), "Parse HTML");` + +### mozilla::AutoGlobalTimelineMarker` + +Similar to the previous RAII class, but doesn't expect a specific docshell, and the marker will be visible in all timeline consumers. This is useful for generic operations that don't involve a particular docshell, or where a docshell isn't accessible. May also only be used on the main thread. + +Example: `AutoGlobalTimelineMarker marker("Some global operation");` diff --git a/docshell/build/components.conf b/docshell/build/components.conf new file mode 100644 index 0000000000..9817f14f64 --- /dev/null +++ b/docshell/build/components.conf @@ -0,0 +1,192 @@ +# -*- 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/. + +about_pages = [ + 'about', + 'addons', + 'buildconfig', + 'certificate', + 'checkerboard', + 'config', + 'crashcontent', + 'crashparent', + 'crashgpu', + 'credits', + 'httpsonlyerror', + 'license', + 'logging', + 'logo', + 'memory', + 'mozilla', + 'neterror', + 'networking', + 'performance', + 'plugins', + 'processes', + 'serviceworkers', + 'srcdoc', + 'support', + 'telemetry', + 'translations', + 'url-classifier', + 'webrtc', +] + +if defined('MOZ_CRASHREPORTER'): + about_pages.append('crashes') +if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android': + about_pages.append('profiles') +if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] == 'windows': + about_pages.append('third-party') + about_pages.append('windows-messages') +if not defined('MOZ_GLEAN_ANDROID'): + about_pages.append('glean') + +Headers = ['/docshell/build/nsDocShellModule.h'] + +InitFunc = 'mozilla::InitDocShellModule' +UnloadFunc = 'mozilla::UnloadDocShellModule' + +Classes = [ + { + 'name': 'DocLoader', + 'cid': '{057b04d0-0ccf-11d2-beba-00805f8a66dc}', + 'contract_ids': ['@mozilla.org/docloaderservice;1'], + 'type': 'nsDocLoader', + 'headers': ['nsDocLoader.h'], + 'init_method': 'Init', + }, + { + 'name': 'URIFixup', + 'js_name': 'uriFixup', + 'cid': '{c6cf88b7-452e-47eb-bdc9-86e3561648ef}', + 'contract_ids': ['@mozilla.org/docshell/uri-fixup;1'], + 'interfaces': ['nsIURIFixup'], + 'esModule': 'resource://gre/modules/URIFixup.sys.mjs', + 'singleton': True, + 'constructor': 'URIFixup', + }, + { + 'cid': '{33d75835-722f-42c0-89cc-44f328e56a86}', + 'contract_ids': ['@mozilla.org/docshell/uri-fixup-info;1'], + 'esModule': 'resource://gre/modules/URIFixup.sys.mjs', + 'constructor': 'URIFixupInfo', + }, + { + 'cid': '{56ebedd4-6ccf-48e8-bdae-adc77f044567}', + 'contract_ids': [ + '@mozilla.org/network/protocol/about;1?what=%s' % path + for path in about_pages + ], + 'legacy_constructor': 'nsAboutRedirector::Create', + 'headers': ['/docshell/base/nsAboutRedirector.h'], + }, + { + 'name': 'ExternalProtocolHandler', + 'cid': '{bd6390c8-fbea-11d4-98f6-001083010e9b}', + 'contract_ids': ['@mozilla.org/network/protocol;1?name=default'], + 'type': 'nsExternalProtocolHandler', + 'headers': ['/uriloader/exthandler/nsExternalProtocolHandler.h'], + 'protocol_config': { + 'scheme': 'default', + 'flags': [ + 'URI_NORELATIVE', + 'URI_NOAUTH', + 'URI_LOADABLE_BY_ANYONE', + 'URI_NON_PERSISTABLE', + 'URI_DOES_NOT_RETURN_DATA', + ], + 'default_port': 0, + }, + 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS, + }, + { + 'cid': '{95790842-75a0-430d-98bf-f5ce3788ea6d}', + 'contract_ids': ['@mozilla.org/ospermissionrequest;1'], + 'type': 'nsOSPermissionRequest', + 'headers': ['nsOSPermissionRequest.h'], + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'name': 'Prefetch', + 'cid': '{6b8bdffc-3394-417d-be83-a81b7c0f63bf}', + 'contract_ids': ['@mozilla.org/prefetch-service;1'], + 'type': 'nsPrefetchService', + 'headers': ['/uriloader/prefetch/nsPrefetchService.h'], + 'init_method': 'Init', + }, + { + 'cid': '{c4b6fb7c-bfb1-49dc-a65f-035796524b53}', + 'contract_ids': ['@mozilla.org/uriloader/handler-service;1'], + 'type': 'nsIHandlerService', + 'headers': ['ContentHandlerService.h'], + 'constructor': 'mozilla::dom::ContentHandlerService::Create', + }, + { + 'cid': '{bc0017e3-2438-47be-a567-41db58f17627}', + 'contract_ids': ['@mozilla.org/uriloader/local-handler-app;1'], + 'type': 'PlatformLocalHandlerApp_t', + 'headers': ['/uriloader/exthandler/nsLocalHandlerApp.h'], + }, + { + 'name': 'URILoader', + 'cid': '{9f6d5d40-90e7-11d3-af80-00a024ffc08c}', + 'contract_ids': ['@mozilla.org/uriloader;1'], + 'type': 'nsURILoader', + 'headers': ['nsURILoader.h'], + }, + { + 'cid': '{f30bc0a2-958b-4287-bf62-ce38ba0c811e}', + 'contract_ids': ['@mozilla.org/webnavigation-info;1'], + 'type': 'nsWebNavigationInfo', + 'headers': ['/docshell/base/nsWebNavigationInfo.h'], + }, +] + +if defined('MOZ_ENABLE_DBUS'): + Classes += [ + { + 'name': 'DBusHandlerApp', + 'cid': '{6c3c274b-4cbf-4bb5-a635-05ad2cbb6535}', + 'contract_ids': ['@mozilla.org/uriloader/dbus-handler-app;1'], + 'type': 'nsDBusHandlerApp', + 'headers': ['/uriloader/exthandler/nsDBusHandlerApp.h'], + }, + ] + +if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] == 'android': + Classes += [ + # Android has its own externel-helper-app-service, so we omit + # that here for nsExternalHelperAppService. + { + 'cid': '{a7f800e0-4306-11d4-98d0-001083010e9b}', + 'contract_ids': [ + '@mozilla.org/mime;1', + '@mozilla.org/uriloader/external-protocol-service;1', + ], + 'type': 'nsExternalHelperAppService', + 'constructor': 'nsExternalHelperAppService::GetSingleton', + 'headers': ['nsExternalHelperAppService.h'], + 'init_method': 'Init', + 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS, + }, + ] +else: + Classes += [ + { + 'cid': '{a7f800e0-4306-11d4-98d0-001083010e9b}', + 'contract_ids': [ + '@mozilla.org/mime;1', + '@mozilla.org/uriloader/external-helper-app-service;1', + '@mozilla.org/uriloader/external-protocol-service;1', + ], + 'type': 'nsExternalHelperAppService', + 'constructor': 'nsExternalHelperAppService::GetSingleton', + 'headers': ['nsExternalHelperAppService.h'], + 'init_method': 'Init', + 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS, + }, + ] diff --git a/docshell/build/moz.build b/docshell/build/moz.build new file mode 100644 index 0000000000..d9fd81848e --- /dev/null +++ b/docshell/build/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/. + +EXPORTS += [ + "nsDocShellCID.h", +] + +SOURCES += [ + "nsDocShellModule.cpp", +] + +LOCAL_INCLUDES += [ + "/docshell/shistory", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/docshell/build/nsDocShellCID.h b/docshell/build/nsDocShellCID.h new file mode 100644 index 0000000000..9a6a90d87a --- /dev/null +++ b/docshell/build/nsDocShellCID.h @@ -0,0 +1,59 @@ +/* -*- 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/. */ + +#ifndef nsDocShellCID_h__ +#define nsDocShellCID_h__ + +/** + * A contract that can be used to get a service that provides + * meta-information about nsIWebNavigation objects' capabilities. + * @implements nsIWebNavigationInfo + */ +#define NS_WEBNAVIGATION_INFO_CONTRACTID "@mozilla.org/webnavigation-info;1" + +/** + * Contract ID to obtain the IHistory interface. This is a non-scriptable + * interface used to interact with history in an asynchronous manner. + */ +#define NS_IHISTORY_CONTRACTID "@mozilla.org/browser/history;1" + +/** + * An observer service topic that can be listened to to catch creation + * of content browsing areas (both toplevel ones and subframes). The + * subject of the notification will be the nsIWebNavigation being + * created. At this time the additional data wstring is not defined + * to be anything in particular. + */ +#define NS_WEBNAVIGATION_CREATE "webnavigation-create" + +/** + * An observer service topic that can be listened to to catch creation + * of chrome browsing areas (both toplevel ones and subframes). The + * subject of the notification will be the nsIWebNavigation being + * created. At this time the additional data wstring is not defined + * to be anything in particular. + */ +#define NS_CHROME_WEBNAVIGATION_CREATE "chrome-webnavigation-create" + +/** + * An observer service topic that can be listened to to catch destruction + * of content browsing areas (both toplevel ones and subframes). The + * subject of the notification will be the nsIWebNavigation being + * destroyed. At this time the additional data wstring is not defined + * to be anything in particular. + */ +#define NS_WEBNAVIGATION_DESTROY "webnavigation-destroy" + +/** + * An observer service topic that can be listened to to catch destruction + * of chrome browsing areas (both toplevel ones and subframes). The + * subject of the notification will be the nsIWebNavigation being + * destroyed. At this time the additional data wstring is not defined + * to be anything in particular. + */ +#define NS_CHROME_WEBNAVIGATION_DESTROY "chrome-webnavigation-destroy" + +#endif // nsDocShellCID_h__ diff --git a/docshell/build/nsDocShellModule.cpp b/docshell/build/nsDocShellModule.cpp new file mode 100644 index 0000000000..8602497a57 --- /dev/null +++ b/docshell/build/nsDocShellModule.cpp @@ -0,0 +1,25 @@ +/* -*- 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/. */ + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/BrowsingContextGroup.h" + +// session history +#include "nsSHEntryShared.h" +#include "nsSHistory.h" + +namespace mozilla { + +// The one time initialization for this module +nsresult InitDocShellModule() { + mozilla::dom::BrowsingContext::Init(); + + return NS_OK; +} + +void UnloadDocShellModule() { nsSHistory::Shutdown(); } + +} // namespace mozilla diff --git a/docshell/build/nsDocShellModule.h b/docshell/build/nsDocShellModule.h new file mode 100644 index 0000000000..c64f3ad8a9 --- /dev/null +++ b/docshell/build/nsDocShellModule.h @@ -0,0 +1,20 @@ +/* -*- 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/. */ + +#ifndef nsDocShellModule_h +#define nsDocShellModule_h + +#include "nscore.h" + +namespace mozilla { + +nsresult InitDocShellModule(); + +void UnloadDocShellModule(); + +} // namespace mozilla + +#endif diff --git a/docshell/moz.build b/docshell/moz.build new file mode 100644 index 0000000000..b60e0f66ad --- /dev/null +++ b/docshell/moz.build @@ -0,0 +1,48 @@ +# -*- 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", "DOM: Navigation") + +if CONFIG["MOZ_BUILD_APP"] == "browser": + DEFINES["MOZ_BUILD_APP_IS_BROWSER"] = True + +DIRS += [ + "base", + "shistory", + "build", +] + +XPCSHELL_TESTS_MANIFESTS += [ + "test/unit/xpcshell.ini", + "test/unit_ipc/xpcshell.ini", +] + +MOCHITEST_MANIFESTS += [ + "test/iframesandbox/mochitest.ini", + "test/mochitest/mochitest.ini", + "test/navigation/mochitest.ini", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "test/chrome/chrome.ini", +] + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.ini", + "test/navigation/browser.ini", +] + +TEST_HARNESS_FILES.testing.mochitest.tests.docshell.test.chrome += [ + "test/chrome/215405_nocache.html", + "test/chrome/215405_nocache.html^headers^", + "test/chrome/215405_nostore.html", + "test/chrome/215405_nostore.html^headers^", + "test/chrome/allowContentRetargeting.sjs", + "test/chrome/blue.png", + "test/chrome/bug89419.sjs", + "test/chrome/red.png", +] diff --git a/docshell/shistory/ChildSHistory.cpp b/docshell/shistory/ChildSHistory.cpp new file mode 100644 index 0000000000..87b6a7b3fc --- /dev/null +++ b/docshell/shistory/ChildSHistory.cpp @@ -0,0 +1,294 @@ +/* -*- 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/. */ + +#include "mozilla/dom/ChildSHistory.h" +#include "mozilla/dom/ChildSHistoryBinding.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentFrameMessageManager.h" +#include "nsIXULRuntime.h" +#include "nsComponentManagerUtils.h" +#include "nsSHEntry.h" +#include "nsSHistory.h" +#include "nsDocShell.h" +#include "nsXULAppAPI.h" + +extern mozilla::LazyLogModule gSHLog; + +namespace mozilla { +namespace dom { + +ChildSHistory::ChildSHistory(BrowsingContext* aBrowsingContext) + : mBrowsingContext(aBrowsingContext) {} + +ChildSHistory::~ChildSHistory() { + if (mHistory) { + static_cast<nsSHistory*>(mHistory.get())->SetBrowsingContext(nullptr); + } +} + +void ChildSHistory::SetBrowsingContext(BrowsingContext* aBrowsingContext) { + mBrowsingContext = aBrowsingContext; +} + +void ChildSHistory::SetIsInProcess(bool aIsInProcess) { + if (!aIsInProcess) { + MOZ_ASSERT_IF(mozilla::SessionHistoryInParent(), !mHistory); + if (!mozilla::SessionHistoryInParent()) { + RemovePendingHistoryNavigations(); + if (mHistory) { + static_cast<nsSHistory*>(mHistory.get())->SetBrowsingContext(nullptr); + mHistory = nullptr; + } + } + + return; + } + + if (mHistory || mozilla::SessionHistoryInParent()) { + return; + } + + mHistory = new nsSHistory(mBrowsingContext); +} + +int32_t ChildSHistory::Count() { + if (mozilla::SessionHistoryInParent()) { + uint32_t length = mLength; + for (uint32_t i = 0; i < mPendingSHistoryChanges.Length(); ++i) { + length += mPendingSHistoryChanges[i].mLengthDelta; + } + + return length; + } + return mHistory->GetCount(); +} + +int32_t ChildSHistory::Index() { + if (mozilla::SessionHistoryInParent()) { + uint32_t index = mIndex; + for (uint32_t i = 0; i < mPendingSHistoryChanges.Length(); ++i) { + index += mPendingSHistoryChanges[i].mIndexDelta; + } + + return index; + } + int32_t index; + mHistory->GetIndex(&index); + return index; +} + +nsID ChildSHistory::AddPendingHistoryChange() { + int32_t indexDelta = 1; + int32_t lengthDelta = (Index() + indexDelta) - (Count() - 1); + return AddPendingHistoryChange(indexDelta, lengthDelta); +} + +nsID ChildSHistory::AddPendingHistoryChange(int32_t aIndexDelta, + int32_t aLengthDelta) { + nsID changeID = nsID::GenerateUUID(); + PendingSHistoryChange change = {changeID, aIndexDelta, aLengthDelta}; + mPendingSHistoryChanges.AppendElement(change); + return changeID; +} + +void ChildSHistory::SetIndexAndLength(uint32_t aIndex, uint32_t aLength, + const nsID& aChangeID) { + mIndex = aIndex; + mLength = aLength; + mPendingSHistoryChanges.RemoveElementsBy( + [aChangeID](const PendingSHistoryChange& aChange) { + return aChange.mChangeID == aChangeID; + }); +} + +void ChildSHistory::Reload(uint32_t aReloadFlags, ErrorResult& aRv) { + if (mozilla::SessionHistoryInParent()) { + if (XRE_IsParentProcess()) { + nsCOMPtr<nsISHistory> shistory = + mBrowsingContext->Canonical()->GetSessionHistory(); + if (shistory) { + aRv = shistory->Reload(aReloadFlags); + } + } else { + ContentChild::GetSingleton()->SendHistoryReload(mBrowsingContext, + aReloadFlags); + } + + return; + } + nsCOMPtr<nsISHistory> shistory = mHistory; + aRv = shistory->Reload(aReloadFlags); +} + +bool ChildSHistory::CanGo(int32_t aOffset) { + CheckedInt<int32_t> index = Index(); + index += aOffset; + if (!index.isValid()) { + return false; + } + return index.value() < Count() && index.value() >= 0; +} + +void ChildSHistory::Go(int32_t aOffset, bool aRequireUserInteraction, + bool aUserActivation, ErrorResult& aRv) { + CheckedInt<int32_t> index = Index(); + MOZ_LOG( + gSHLog, LogLevel::Debug, + ("ChildSHistory::Go(%d), current index = %d", aOffset, index.value())); + if (aRequireUserInteraction && aOffset != -1 && aOffset != 1) { + NS_ERROR( + "aRequireUserInteraction may only be used with an offset of -1 or 1"); + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + while (true) { + index += aOffset; + if (!index.isValid()) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Check for user interaction if desired, except for the first and last + // history entries. We compare with >= to account for the case where + // aOffset >= Count(). + if (!aRequireUserInteraction || index.value() >= Count() - 1 || + index.value() <= 0) { + break; + } + if (mHistory && mHistory->HasUserInteractionAtIndex(index.value())) { + break; + } + } + + GotoIndex(index.value(), aOffset, aRequireUserInteraction, aUserActivation, + aRv); +} + +void ChildSHistory::AsyncGo(int32_t aOffset, bool aRequireUserInteraction, + bool aUserActivation, CallerType aCallerType, + ErrorResult& aRv) { + CheckedInt<int32_t> index = Index(); + MOZ_LOG(gSHLog, LogLevel::Debug, + ("ChildSHistory::AsyncGo(%d), current index = %d", aOffset, + index.value())); + nsresult rv = mBrowsingContext->CheckLocationChangeRateLimit(aCallerType); + if (NS_FAILED(rv)) { + MOZ_LOG(gSHLog, LogLevel::Debug, ("Rejected")); + aRv.Throw(rv); + return; + } + + RefPtr<PendingAsyncHistoryNavigation> asyncNav = + new PendingAsyncHistoryNavigation(this, aOffset, aRequireUserInteraction, + aUserActivation); + mPendingNavigations.insertBack(asyncNav); + NS_DispatchToCurrentThread(asyncNav.forget()); +} + +void ChildSHistory::GotoIndex(int32_t aIndex, int32_t aOffset, + bool aRequireUserInteraction, + bool aUserActivation, ErrorResult& aRv) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("ChildSHistory::GotoIndex(%d, %d), epoch %" PRIu64, aIndex, aOffset, + mHistoryEpoch)); + if (mozilla::SessionHistoryInParent()) { + if (!mPendingEpoch) { + mPendingEpoch = true; + RefPtr<ChildSHistory> self(this); + NS_DispatchToCurrentThread( + NS_NewRunnableFunction("UpdateEpochRunnable", [self] { + self->mHistoryEpoch++; + self->mPendingEpoch = false; + })); + } + + nsCOMPtr<nsISHistory> shistory = mHistory; + RefPtr<BrowsingContext> bc = mBrowsingContext; + bc->HistoryGo( + aOffset, mHistoryEpoch, aRequireUserInteraction, aUserActivation, + [shistory](Maybe<int32_t>&& aRequestedIndex) { + // FIXME Should probably only do this for non-fission. + if (aRequestedIndex.isSome() && shistory) { + shistory->InternalSetRequestedIndex(aRequestedIndex.value()); + } + }); + } else { + nsCOMPtr<nsISHistory> shistory = mHistory; + aRv = shistory->GotoIndex(aIndex, aUserActivation); + } +} + +void ChildSHistory::RemovePendingHistoryNavigations() { + // Per the spec, this generally shouldn't remove all navigations - it + // depends if they're in the same document family or not. We don't do + // that. Also with SessionHistoryInParent, this can only abort AsyncGo's + // that have not yet been sent to the parent - see discussion of point + // 2.2 in comments in nsDocShell::UpdateURLAndHistory() + MOZ_LOG(gSHLog, LogLevel::Debug, + ("ChildSHistory::RemovePendingHistoryNavigations: %zu", + mPendingNavigations.length())); + mPendingNavigations.clear(); +} + +void ChildSHistory::EvictLocalContentViewers() { + if (!mozilla::SessionHistoryInParent()) { + mHistory->EvictAllContentViewers(); + } +} + +nsISHistory* ChildSHistory::GetLegacySHistory(ErrorResult& aError) { + if (mozilla::SessionHistoryInParent()) { + aError.ThrowTypeError( + "legacySHistory is not available with session history in the parent."); + return nullptr; + } + + MOZ_RELEASE_ASSERT(mHistory); + return mHistory; +} + +nsISHistory* ChildSHistory::LegacySHistory() { + IgnoredErrorResult ignore; + nsISHistory* shistory = GetLegacySHistory(ignore); + MOZ_RELEASE_ASSERT(shistory); + return shistory; +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChildSHistory) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ChildSHistory) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ChildSHistory) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ChildSHistory) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ChildSHistory) + if (tmp->mHistory) { + static_cast<nsSHistory*>(tmp->mHistory.get())->SetBrowsingContext(nullptr); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBrowsingContext, mHistory) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ChildSHistory) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBrowsingContext, mHistory) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +JSObject* ChildSHistory::WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) { + return ChildSHistory_Binding::Wrap(cx, this, aGivenProto); +} + +nsISupports* ChildSHistory::GetParentObject() const { + return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); +} + +} // namespace dom +} // namespace mozilla diff --git a/docshell/shistory/ChildSHistory.h b/docshell/shistory/ChildSHistory.h new file mode 100644 index 0000000000..031c91c7da --- /dev/null +++ b/docshell/shistory/ChildSHistory.h @@ -0,0 +1,149 @@ +/* -*- 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/. */ + +/** + * ChildSHistory represents a view of session history from a child process. It + * exposes getters for some cached history state, and mutators which are + * implemented by communicating with the actual history storage. + * + * NOTE: Currently session history is in transition, meaning that we're still + * using the legacy nsSHistory class internally. The API exposed from this class + * should be only the API which we expect to expose when this transition is + * complete, and special cases will need to call through the LegacySHistory() + * getters. + */ + +#ifndef mozilla_dom_ChildSHistory_h +#define mozilla_dom_ChildSHistory_h + +#include "nsCOMPtr.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsWrapperCache.h" +#include "nsThreadUtils.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/LinkedList.h" +#include "nsID.h" + +class nsISHEntry; +class nsISHistory; + +namespace mozilla::dom { + +class BrowsingContext; + +class ChildSHistory : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ChildSHistory) + nsISupports* GetParentObject() const; + JSObject* WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) override; + + explicit ChildSHistory(BrowsingContext* aBrowsingContext); + + void SetBrowsingContext(BrowsingContext* aBrowsingContext); + + // Create or destroy the session history implementation in the child process. + // This can be removed once session history is stored exclusively in the + // parent process. + void SetIsInProcess(bool aIsInProcess); + bool IsInProcess() { return !!mHistory; } + + int32_t Count(); + int32_t Index(); + + /** + * Reload the current entry in the session history. + */ + void Reload(uint32_t aReloadFlags, ErrorResult& aRv); + + /** + * The CanGo and Go methods are called with an offset from the current index. + * Positive numbers go forward in history, while negative numbers go + * backwards. + */ + bool CanGo(int32_t aOffset); + void Go(int32_t aOffset, bool aRequireUserInteraction, bool aUserActivation, + ErrorResult& aRv); + void AsyncGo(int32_t aOffset, bool aRequireUserInteraction, + bool aUserActivation, CallerType aCallerType, ErrorResult& aRv); + + // aIndex is the new index, and aOffset is the offset between new and current. + void GotoIndex(int32_t aIndex, int32_t aOffset, bool aRequireUserInteraction, + bool aUserActivation, ErrorResult& aRv); + + void RemovePendingHistoryNavigations(); + + /** + * Evicts all content viewers within the current process. + */ + void EvictLocalContentViewers(); + + // GetLegacySHistory and LegacySHistory have been deprecated. Don't + // use these, but instead handle the interaction with nsISHistory in + // the parent process. + nsISHistory* GetLegacySHistory(ErrorResult& aError); + nsISHistory* LegacySHistory(); + + void SetIndexAndLength(uint32_t aIndex, uint32_t aLength, + const nsID& aChangeId); + nsID AddPendingHistoryChange(); + nsID AddPendingHistoryChange(int32_t aIndexDelta, int32_t aLengthDelta); + + private: + virtual ~ChildSHistory(); + + class PendingAsyncHistoryNavigation + : public Runnable, + public mozilla::LinkedListElement<PendingAsyncHistoryNavigation> { + public: + PendingAsyncHistoryNavigation(ChildSHistory* aHistory, int32_t aOffset, + bool aRequireUserInteraction, + bool aUserActivation) + : Runnable("PendingAsyncHistoryNavigation"), + mHistory(aHistory), + mRequireUserInteraction(aRequireUserInteraction), + mUserActivation(aUserActivation), + mOffset(aOffset) {} + + NS_IMETHOD Run() override { + if (isInList()) { + remove(); + mHistory->Go(mOffset, mRequireUserInteraction, mUserActivation, + IgnoreErrors()); + } + return NS_OK; + } + + private: + RefPtr<ChildSHistory> mHistory; + bool mRequireUserInteraction; + bool mUserActivation; + int32_t mOffset; + }; + + RefPtr<BrowsingContext> mBrowsingContext; + nsCOMPtr<nsISHistory> mHistory; + // Can be removed once history-in-parent is the only way + mozilla::LinkedList<PendingAsyncHistoryNavigation> mPendingNavigations; + int32_t mIndex = -1; + int32_t mLength = 0; + + struct PendingSHistoryChange { + nsID mChangeID; + int32_t mIndexDelta; + int32_t mLengthDelta; + }; + AutoTArray<PendingSHistoryChange, 2> mPendingSHistoryChanges; + + // Needs to start 1 above default epoch in parent + uint64_t mHistoryEpoch = 1; + bool mPendingEpoch = false; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_ChildSHistory_h */ diff --git a/docshell/shistory/SessionHistoryEntry.cpp b/docshell/shistory/SessionHistoryEntry.cpp new file mode 100644 index 0000000000..67829e630e --- /dev/null +++ b/docshell/shistory/SessionHistoryEntry.cpp @@ -0,0 +1,1828 @@ +/* -*- 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/. */ + +#include "SessionHistoryEntry.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "nsDocShell.h" +#include "nsDocShellLoadState.h" +#include "nsFrameLoader.h" +#include "nsIFormPOSTActionChannel.h" +#include "nsIHttpChannel.h" +#include "nsIUploadChannel2.h" +#include "nsIXULRuntime.h" +#include "nsSHEntryShared.h" +#include "nsSHistory.h" +#include "nsStreamUtils.h" +#include "nsStructuredCloneContainer.h" +#include "nsXULAppAPI.h" +#include "mozilla/PresState.h" +#include "mozilla/StaticPrefs_fission.h" + +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/CSPMessageUtils.h" +#include "mozilla/dom/DocumentBinding.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/dom/ReferrerInfoUtils.h" +#include "mozilla/ipc/IPDLParamTraits.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/ipc/URIUtils.h" + +extern mozilla::LazyLogModule gSHLog; + +namespace mozilla { +namespace dom { + +SessionHistoryInfo::SessionHistoryInfo(nsDocShellLoadState* aLoadState, + nsIChannel* aChannel) + : mURI(aLoadState->URI()), + mOriginalURI(aLoadState->OriginalURI()), + mResultPrincipalURI(aLoadState->ResultPrincipalURI()), + mUnstrippedURI(aLoadState->GetUnstrippedURI()), + mLoadType(aLoadState->LoadType()), + mSrcdocData(aLoadState->SrcdocData().IsVoid() + ? Nothing() + : Some(aLoadState->SrcdocData())), + mBaseURI(aLoadState->BaseURI()), + mLoadReplace(aLoadState->LoadReplace()), + mHasUserInteraction(false), + mHasUserActivation(aLoadState->HasValidUserGestureActivation()), + mSharedState(SharedState::Create( + aLoadState->TriggeringPrincipal(), aLoadState->PrincipalToInherit(), + aLoadState->PartitionedPrincipalToInherit(), aLoadState->Csp(), + /* FIXME Is this correct? */ + aLoadState->TypeHint())) { + // Pull the upload stream off of the channel instead of the load state, as + // ownership has already been transferred from the load state to the channel. + if (nsCOMPtr<nsIUploadChannel2> postChannel = do_QueryInterface(aChannel)) { + int64_t contentLength; + MOZ_ALWAYS_SUCCEEDS(postChannel->CloneUploadStream( + &contentLength, getter_AddRefs(mPostData))); + MOZ_ASSERT_IF(mPostData, NS_InputStreamIsCloneable(mPostData)); + } + + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel)) { + mReferrerInfo = httpChannel->GetReferrerInfo(); + } + + MaybeUpdateTitleFromURI(); +} + +SessionHistoryInfo::SessionHistoryInfo( + const SessionHistoryInfo& aSharedStateFrom, nsIURI* aURI) + : mURI(aURI), mSharedState(aSharedStateFrom.mSharedState) { + MaybeUpdateTitleFromURI(); +} + +SessionHistoryInfo::SessionHistoryInfo( + nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, const nsACString& aContentType) + : mURI(aURI), + mSharedState(SharedState::Create( + aTriggeringPrincipal, aPrincipalToInherit, + aPartitionedPrincipalToInherit, aCsp, aContentType)) { + MaybeUpdateTitleFromURI(); +} + +SessionHistoryInfo::SessionHistoryInfo( + nsIChannel* aChannel, uint32_t aLoadType, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp) { + aChannel->GetURI(getter_AddRefs(mURI)); + mLoadType = aLoadType; + + nsCOMPtr<nsILoadInfo> loadInfo; + aChannel->GetLoadInfo(getter_AddRefs(loadInfo)); + + loadInfo->GetResultPrincipalURI(getter_AddRefs(mResultPrincipalURI)); + loadInfo->GetUnstrippedURI(getter_AddRefs(mUnstrippedURI)); + loadInfo->GetTriggeringPrincipal( + getter_AddRefs(mSharedState.Get()->mTriggeringPrincipal)); + loadInfo->GetPrincipalToInherit( + getter_AddRefs(mSharedState.Get()->mPrincipalToInherit)); + + mSharedState.Get()->mPartitionedPrincipalToInherit = + aPartitionedPrincipalToInherit; + mSharedState.Get()->mCsp = aCsp; + aChannel->GetContentType(mSharedState.Get()->mContentType); + aChannel->GetOriginalURI(getter_AddRefs(mOriginalURI)); + + uint32_t loadFlags; + aChannel->GetLoadFlags(&loadFlags); + mLoadReplace = !!(loadFlags & nsIChannel::LOAD_REPLACE); + + MaybeUpdateTitleFromURI(); + + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel)) { + mReferrerInfo = httpChannel->GetReferrerInfo(); + } +} + +void SessionHistoryInfo::Reset(nsIURI* aURI, const nsID& aDocShellID, + bool aDynamicCreation, + nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, + const nsACString& aContentType) { + mURI = aURI; + mOriginalURI = nullptr; + mResultPrincipalURI = nullptr; + mUnstrippedURI = nullptr; + mReferrerInfo = nullptr; + // Default title is the URL. + nsAutoCString spec; + if (NS_SUCCEEDED(mURI->GetSpec(spec))) { + CopyUTF8toUTF16(spec, mTitle); + } + mPostData = nullptr; + mLoadType = 0; + mScrollPositionX = 0; + mScrollPositionY = 0; + mStateData = nullptr; + mSrcdocData = Nothing(); + mBaseURI = nullptr; + mLoadReplace = false; + mURIWasModified = false; + mScrollRestorationIsManual = false; + mPersist = false; + mHasUserInteraction = false; + mHasUserActivation = false; + + mSharedState.Get()->mTriggeringPrincipal = aTriggeringPrincipal; + mSharedState.Get()->mPrincipalToInherit = aPrincipalToInherit; + mSharedState.Get()->mPartitionedPrincipalToInherit = + aPartitionedPrincipalToInherit; + mSharedState.Get()->mCsp = aCsp; + mSharedState.Get()->mContentType = aContentType; + mSharedState.Get()->mLayoutHistoryState = nullptr; +} + +void SessionHistoryInfo::MaybeUpdateTitleFromURI() { + if (mTitle.IsEmpty() && mURI) { + // Default title is the URL. + nsAutoCString spec; + if (NS_SUCCEEDED(mURI->GetSpec(spec))) { + AppendUTF8toUTF16(spec, mTitle); + } + } +} + +already_AddRefed<nsIInputStream> SessionHistoryInfo::GetPostData() const { + // Return a clone of our post data stream. Our caller will either be + // transferring this stream to a different SessionHistoryInfo, or passing it + // off to necko/another process which will consume it, and we want to preserve + // our local instance. + nsCOMPtr<nsIInputStream> postData; + if (mPostData) { + MOZ_ALWAYS_SUCCEEDS( + NS_CloneInputStream(mPostData, getter_AddRefs(postData))); + } + return postData.forget(); +} + +void SessionHistoryInfo::SetPostData(nsIInputStream* aPostData) { + MOZ_ASSERT_IF(aPostData, NS_InputStreamIsCloneable(aPostData)); + mPostData = aPostData; +} + +uint64_t SessionHistoryInfo::SharedId() const { + return mSharedState.Get()->mId; +} + +nsILayoutHistoryState* SessionHistoryInfo::GetLayoutHistoryState() { + return mSharedState.Get()->mLayoutHistoryState; +} + +void SessionHistoryInfo::SetLayoutHistoryState(nsILayoutHistoryState* aState) { + mSharedState.Get()->mLayoutHistoryState = aState; + if (mSharedState.Get()->mLayoutHistoryState) { + mSharedState.Get()->mLayoutHistoryState->SetScrollPositionOnly( + !mSharedState.Get()->mSaveLayoutState); + } +} + +nsIPrincipal* SessionHistoryInfo::GetTriggeringPrincipal() const { + return mSharedState.Get()->mTriggeringPrincipal; +} + +nsIPrincipal* SessionHistoryInfo::GetPrincipalToInherit() const { + return mSharedState.Get()->mPrincipalToInherit; +} + +nsIPrincipal* SessionHistoryInfo::GetPartitionedPrincipalToInherit() const { + return mSharedState.Get()->mPartitionedPrincipalToInherit; +} + +nsIContentSecurityPolicy* SessionHistoryInfo::GetCsp() const { + return mSharedState.Get()->mCsp; +} + +uint32_t SessionHistoryInfo::GetCacheKey() const { + return mSharedState.Get()->mCacheKey; +} + +void SessionHistoryInfo::SetCacheKey(uint32_t aCacheKey) { + mSharedState.Get()->mCacheKey = aCacheKey; +} + +bool SessionHistoryInfo::IsSubFrame() const { + return mSharedState.Get()->mIsFrameNavigation; +} + +void SessionHistoryInfo::SetSaveLayoutStateFlag(bool aSaveLayoutStateFlag) { + MOZ_ASSERT(XRE_IsParentProcess()); + static_cast<SHEntrySharedParentState*>(mSharedState.Get())->mSaveLayoutState = + aSaveLayoutStateFlag; +} + +void SessionHistoryInfo::FillLoadInfo(nsDocShellLoadState& aLoadState) const { + aLoadState.SetOriginalURI(mOriginalURI); + aLoadState.SetMaybeResultPrincipalURI(Some(mResultPrincipalURI)); + aLoadState.SetUnstrippedURI(mUnstrippedURI); + aLoadState.SetLoadReplace(mLoadReplace); + nsCOMPtr<nsIInputStream> postData = GetPostData(); + aLoadState.SetPostDataStream(postData); + aLoadState.SetReferrerInfo(mReferrerInfo); + + aLoadState.SetTypeHint(mSharedState.Get()->mContentType); + aLoadState.SetTriggeringPrincipal(mSharedState.Get()->mTriggeringPrincipal); + aLoadState.SetPrincipalToInherit(mSharedState.Get()->mPrincipalToInherit); + aLoadState.SetPartitionedPrincipalToInherit( + mSharedState.Get()->mPartitionedPrincipalToInherit); + aLoadState.SetCsp(mSharedState.Get()->mCsp); + + // Do not inherit principal from document (security-critical!); + uint32_t flags = nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_NONE; + + // Passing nullptr as aSourceDocShell gives the same behaviour as before + // aSourceDocShell was introduced. According to spec we should be passing + // the source browsing context that was used when the history entry was + // first created. bug 947716 has been created to address this issue. + nsAutoString srcdoc; + nsCOMPtr<nsIURI> baseURI; + if (mSrcdocData) { + srcdoc = mSrcdocData.value(); + baseURI = mBaseURI; + flags |= nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_IS_SRCDOC; + } else { + srcdoc = VoidString(); + } + aLoadState.SetSrcdocData(srcdoc); + aLoadState.SetBaseURI(baseURI); + aLoadState.SetInternalLoadFlags(flags); + + aLoadState.SetFirstParty(true); + + // When we create a load state from the history info we already know if + // https-first was able to upgrade the request from http to https. There is no + // point in re-retrying to upgrade. + aLoadState.SetIsExemptFromHTTPSOnlyMode(true); +} +/* static */ +SessionHistoryInfo::SharedState SessionHistoryInfo::SharedState::Create( + nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, const nsACString& aContentType) { + if (XRE_IsParentProcess()) { + return SharedState(new SHEntrySharedParentState( + aTriggeringPrincipal, aPrincipalToInherit, + aPartitionedPrincipalToInherit, aCsp, aContentType)); + } + + return SharedState(MakeUnique<SHEntrySharedState>( + aTriggeringPrincipal, aPrincipalToInherit, aPartitionedPrincipalToInherit, + aCsp, aContentType)); +} + +SessionHistoryInfo::SharedState::SharedState() { Init(); } + +SessionHistoryInfo::SharedState::SharedState( + const SessionHistoryInfo::SharedState& aOther) { + Init(aOther); +} + +SessionHistoryInfo::SharedState::SharedState( + const Maybe<const SessionHistoryInfo::SharedState&>& aOther) { + if (aOther.isSome()) { + Init(aOther.ref()); + } else { + Init(); + } +} + +SessionHistoryInfo::SharedState::~SharedState() { + if (XRE_IsParentProcess()) { + mParent + .RefPtr<SHEntrySharedParentState>::~RefPtr<SHEntrySharedParentState>(); + } else { + mChild.UniquePtr<SHEntrySharedState>::~UniquePtr<SHEntrySharedState>(); + } +} + +SessionHistoryInfo::SharedState& SessionHistoryInfo::SharedState::operator=( + const SessionHistoryInfo::SharedState& aOther) { + if (this != &aOther) { + if (XRE_IsParentProcess()) { + mParent = aOther.mParent; + } else { + mChild = MakeUnique<SHEntrySharedState>(*aOther.mChild); + } + } + return *this; +} + +SHEntrySharedState* SessionHistoryInfo::SharedState::Get() const { + if (XRE_IsParentProcess()) { + return mParent; + } + + return mChild.get(); +} + +void SessionHistoryInfo::SharedState::ChangeId(uint64_t aId) { + if (XRE_IsParentProcess()) { + mParent->ChangeId(aId); + } else { + mChild->mId = aId; + } +} + +void SessionHistoryInfo::SharedState::Init() { + if (XRE_IsParentProcess()) { + new (&mParent) + RefPtr<SHEntrySharedParentState>(new SHEntrySharedParentState()); + } else { + new (&mChild) + UniquePtr<SHEntrySharedState>(MakeUnique<SHEntrySharedState>()); + } +} + +void SessionHistoryInfo::SharedState::Init( + const SessionHistoryInfo::SharedState& aOther) { + if (XRE_IsParentProcess()) { + new (&mParent) RefPtr<SHEntrySharedParentState>(aOther.mParent); + } else { + new (&mChild) UniquePtr<SHEntrySharedState>( + MakeUnique<SHEntrySharedState>(*aOther.mChild)); + } +} + +static uint64_t gLoadingSessionHistoryInfoLoadId = 0; + +nsTHashMap<nsUint64HashKey, SessionHistoryEntry::LoadingEntry>* + SessionHistoryEntry::sLoadIdToEntry = nullptr; + +LoadingSessionHistoryInfo::LoadingSessionHistoryInfo( + SessionHistoryEntry* aEntry) + : mInfo(aEntry->Info()), mLoadId(++gLoadingSessionHistoryInfoLoadId) { + SessionHistoryEntry::SetByLoadId(mLoadId, aEntry); +} + +LoadingSessionHistoryInfo::LoadingSessionHistoryInfo( + SessionHistoryEntry* aEntry, const LoadingSessionHistoryInfo* aInfo) + : mInfo(aEntry->Info()), + mLoadId(aInfo->mLoadId), + mLoadIsFromSessionHistory(aInfo->mLoadIsFromSessionHistory), + mOffset(aInfo->mOffset), + mLoadingCurrentEntry(aInfo->mLoadingCurrentEntry) { + MOZ_ASSERT(SessionHistoryEntry::GetByLoadId(mLoadId)->mEntry == aEntry); +} + +LoadingSessionHistoryInfo::LoadingSessionHistoryInfo( + const SessionHistoryInfo& aInfo) + : mInfo(aInfo), mLoadId(UINT64_MAX) {} + +already_AddRefed<nsDocShellLoadState> +LoadingSessionHistoryInfo::CreateLoadInfo() const { + RefPtr<nsDocShellLoadState> loadState( + new nsDocShellLoadState(mInfo.GetURI())); + + mInfo.FillLoadInfo(*loadState); + + loadState->SetLoadingSessionHistoryInfo(*this); + + return loadState.forget(); +} + +static uint32_t gEntryID; + +SessionHistoryEntry::LoadingEntry* SessionHistoryEntry::GetByLoadId( + uint64_t aLoadId) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (!sLoadIdToEntry) { + return nullptr; + } + + return sLoadIdToEntry->Lookup(aLoadId).DataPtrOrNull(); +} + +void SessionHistoryEntry::SetByLoadId(uint64_t aLoadId, + SessionHistoryEntry* aEntry) { + if (!sLoadIdToEntry) { + sLoadIdToEntry = new nsTHashMap<nsUint64HashKey, LoadingEntry>(); + } + + MOZ_LOG( + gSHLog, LogLevel::Verbose, + ("SessionHistoryEntry::SetByLoadId(%" PRIu64 " - %p)", aLoadId, aEntry)); + sLoadIdToEntry->InsertOrUpdate( + aLoadId, LoadingEntry{ + .mEntry = aEntry, + .mInfoSnapshotForValidation = + MakeUnique<SessionHistoryInfo>(aEntry->Info()), + }); +} + +void SessionHistoryEntry::RemoveLoadId(uint64_t aLoadId) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (!sLoadIdToEntry) { + return; + } + + MOZ_LOG(gSHLog, LogLevel::Verbose, + ("SHEntry::RemoveLoadId(%" PRIu64 ")", aLoadId)); + sLoadIdToEntry->Remove(aLoadId); +} + +SessionHistoryEntry::SessionHistoryEntry() + : mInfo(new SessionHistoryInfo()), mID(++gEntryID) { + MOZ_ASSERT(mozilla::SessionHistoryInParent()); +} + +SessionHistoryEntry::SessionHistoryEntry(nsDocShellLoadState* aLoadState, + nsIChannel* aChannel) + : mInfo(new SessionHistoryInfo(aLoadState, aChannel)), mID(++gEntryID) { + MOZ_ASSERT(mozilla::SessionHistoryInParent()); +} + +SessionHistoryEntry::SessionHistoryEntry(SessionHistoryInfo* aInfo) + : mInfo(MakeUnique<SessionHistoryInfo>(*aInfo)), mID(++gEntryID) { + MOZ_ASSERT(mozilla::SessionHistoryInParent()); +} + +SessionHistoryEntry::SessionHistoryEntry(const SessionHistoryEntry& aEntry) + : mInfo(MakeUnique<SessionHistoryInfo>(*aEntry.mInfo)), + mParent(aEntry.mParent), + mID(aEntry.mID), + mBCHistoryLength(aEntry.mBCHistoryLength) { + MOZ_ASSERT(mozilla::SessionHistoryInParent()); +} + +SessionHistoryEntry::~SessionHistoryEntry() { + // Null out the mParent pointers on all our kids. + for (nsISHEntry* entry : mChildren) { + if (entry) { + entry->SetParent(nullptr); + } + } + + if (sLoadIdToEntry) { + sLoadIdToEntry->RemoveIf( + [this](auto& aIter) { return aIter.Data().mEntry == this; }); + if (sLoadIdToEntry->IsEmpty()) { + delete sLoadIdToEntry; + sLoadIdToEntry = nullptr; + } + } +} + +NS_IMPL_ISUPPORTS(SessionHistoryEntry, nsISHEntry, SessionHistoryEntry, + nsISupportsWeakReference) + +NS_IMETHODIMP +SessionHistoryEntry::GetURI(nsIURI** aURI) { + nsCOMPtr<nsIURI> uri = mInfo->mURI; + uri.forget(aURI); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetURI(nsIURI* aURI) { + mInfo->mURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetOriginalURI(nsIURI** aOriginalURI) { + nsCOMPtr<nsIURI> originalURI = mInfo->mOriginalURI; + originalURI.forget(aOriginalURI); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetOriginalURI(nsIURI* aOriginalURI) { + mInfo->mOriginalURI = aOriginalURI; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetResultPrincipalURI(nsIURI** aResultPrincipalURI) { + nsCOMPtr<nsIURI> resultPrincipalURI = mInfo->mResultPrincipalURI; + resultPrincipalURI.forget(aResultPrincipalURI); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetResultPrincipalURI(nsIURI* aResultPrincipalURI) { + mInfo->mResultPrincipalURI = aResultPrincipalURI; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetUnstrippedURI(nsIURI** aUnstrippedURI) { + nsCOMPtr<nsIURI> unstrippedURI = mInfo->mUnstrippedURI; + unstrippedURI.forget(aUnstrippedURI); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetUnstrippedURI(nsIURI* aUnstrippedURI) { + mInfo->mUnstrippedURI = aUnstrippedURI; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetLoadReplace(bool* aLoadReplace) { + *aLoadReplace = mInfo->mLoadReplace; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetLoadReplace(bool aLoadReplace) { + mInfo->mLoadReplace = aLoadReplace; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetTitle(nsAString& aTitle) { + aTitle = mInfo->mTitle; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetTitle(const nsAString& aTitle) { + mInfo->SetTitle(aTitle); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetName(nsAString& aName) { + aName = mInfo->mName; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetName(const nsAString& aName) { + mInfo->mName = aName; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetIsSubFrame(bool* aIsSubFrame) { + *aIsSubFrame = SharedInfo()->mIsFrameNavigation; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetIsSubFrame(bool aIsSubFrame) { + SharedInfo()->mIsFrameNavigation = aIsSubFrame; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetHasUserInteraction(bool* aFlag) { + // The back button and menulist deal with root/top-level + // session history entries, thus we annotate only the root entry. + if (!mParent) { + *aFlag = mInfo->mHasUserInteraction; + } else { + nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(this); + root->GetHasUserInteraction(aFlag); + } + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetHasUserInteraction(bool aFlag) { + // The back button and menulist deal with root/top-level + // session history entries, thus we annotate only the root entry. + if (!mParent) { + mInfo->mHasUserInteraction = aFlag; + } else { + nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(this); + root->SetHasUserInteraction(aFlag); + } + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetHasUserActivation(bool* aFlag) { + *aFlag = mInfo->mHasUserActivation; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetHasUserActivation(bool aFlag) { + mInfo->mHasUserActivation = aFlag; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) { + nsCOMPtr<nsIReferrerInfo> referrerInfo = mInfo->mReferrerInfo; + referrerInfo.forget(aReferrerInfo); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + mInfo->mReferrerInfo = aReferrerInfo; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetContentViewer(nsIContentViewer** aContentViewer) { + *aContentViewer = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetContentViewer(nsIContentViewer* aContentViewer) { + MOZ_CRASH("This lives in the child process"); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetIsInBFCache(bool* aResult) { + *aResult = !!SharedInfo()->mFrameLoader; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetSticky(bool* aSticky) { + *aSticky = SharedInfo()->mSticky; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetSticky(bool aSticky) { + SharedInfo()->mSticky = aSticky; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetWindowState(nsISupports** aWindowState) { + MOZ_CRASH("This lives in the child process"); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetWindowState(nsISupports* aWindowState) { + MOZ_CRASH("This lives in the child process"); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetRefreshURIList(nsIMutableArray** aRefreshURIList) { + MOZ_CRASH("This lives in the child process"); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetRefreshURIList(nsIMutableArray* aRefreshURIList) { + MOZ_CRASH("This lives in the child process"); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetPostData(nsIInputStream** aPostData) { + *aPostData = mInfo->GetPostData().take(); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetPostData(nsIInputStream* aPostData) { + mInfo->SetPostData(aPostData); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetHasPostData(bool* aResult) { + *aResult = mInfo->HasPostData(); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetLayoutHistoryState( + nsILayoutHistoryState** aLayoutHistoryState) { + nsCOMPtr<nsILayoutHistoryState> layoutHistoryState = + SharedInfo()->mLayoutHistoryState; + layoutHistoryState.forget(aLayoutHistoryState); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetLayoutHistoryState( + nsILayoutHistoryState* aLayoutHistoryState) { + SharedInfo()->mLayoutHistoryState = aLayoutHistoryState; + if (SharedInfo()->mLayoutHistoryState) { + SharedInfo()->mLayoutHistoryState->SetScrollPositionOnly( + !SharedInfo()->mSaveLayoutState); + } + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetParent(nsISHEntry** aParent) { + nsCOMPtr<nsISHEntry> parent = do_QueryReferent(mParent); + parent.forget(aParent); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetParent(nsISHEntry* aParent) { + mParent = do_GetWeakReference(aParent); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetLoadType(uint32_t* aLoadType) { + *aLoadType = mInfo->mLoadType; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetLoadType(uint32_t aLoadType) { + mInfo->mLoadType = aLoadType; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetID(uint32_t* aID) { + *aID = mID; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetID(uint32_t aID) { + mID = aID; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetCacheKey(uint32_t* aCacheKey) { + *aCacheKey = SharedInfo()->mCacheKey; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetCacheKey(uint32_t aCacheKey) { + SharedInfo()->mCacheKey = aCacheKey; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetSaveLayoutStateFlag(bool* aSaveLayoutStateFlag) { + *aSaveLayoutStateFlag = SharedInfo()->mSaveLayoutState; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetSaveLayoutStateFlag(bool aSaveLayoutStateFlag) { + SharedInfo()->mSaveLayoutState = aSaveLayoutStateFlag; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetContentType(nsACString& aContentType) { + aContentType = SharedInfo()->mContentType; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetContentType(const nsACString& aContentType) { + SharedInfo()->mContentType = aContentType; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetURIWasModified(bool* aURIWasModified) { + *aURIWasModified = mInfo->mURIWasModified; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetURIWasModified(bool aURIWasModified) { + mInfo->mURIWasModified = aURIWasModified; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetTriggeringPrincipal( + nsIPrincipal** aTriggeringPrincipal) { + nsCOMPtr<nsIPrincipal> triggeringPrincipal = + SharedInfo()->mTriggeringPrincipal; + triggeringPrincipal.forget(aTriggeringPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetTriggeringPrincipal( + nsIPrincipal* aTriggeringPrincipal) { + SharedInfo()->mTriggeringPrincipal = aTriggeringPrincipal; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetPrincipalToInherit(nsIPrincipal** aPrincipalToInherit) { + nsCOMPtr<nsIPrincipal> principalToInherit = SharedInfo()->mPrincipalToInherit; + principalToInherit.forget(aPrincipalToInherit); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetPrincipalToInherit(nsIPrincipal* aPrincipalToInherit) { + SharedInfo()->mPrincipalToInherit = aPrincipalToInherit; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetPartitionedPrincipalToInherit( + nsIPrincipal** aPartitionedPrincipalToInherit) { + nsCOMPtr<nsIPrincipal> partitionedPrincipalToInherit = + SharedInfo()->mPartitionedPrincipalToInherit; + partitionedPrincipalToInherit.forget(aPartitionedPrincipalToInherit); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetPartitionedPrincipalToInherit( + nsIPrincipal* aPartitionedPrincipalToInherit) { + SharedInfo()->mPartitionedPrincipalToInherit = aPartitionedPrincipalToInherit; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetCsp(nsIContentSecurityPolicy** aCsp) { + nsCOMPtr<nsIContentSecurityPolicy> csp = SharedInfo()->mCsp; + csp.forget(aCsp); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetCsp(nsIContentSecurityPolicy* aCsp) { + SharedInfo()->mCsp = aCsp; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetStateData(nsIStructuredCloneContainer** aStateData) { + RefPtr<nsStructuredCloneContainer> stateData = mInfo->mStateData; + stateData.forget(aStateData); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetStateData(nsIStructuredCloneContainer* aStateData) { + mInfo->mStateData = static_cast<nsStructuredCloneContainer*>(aStateData); + return NS_OK; +} + +const nsID& SessionHistoryEntry::DocshellID() const { + return SharedInfo()->mDocShellID; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetDocshellID(nsID& aDocshellID) { + aDocshellID = DocshellID(); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetDocshellID(const nsID& aDocshellID) { + SharedInfo()->mDocShellID = aDocshellID; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetIsSrcdocEntry(bool* aIsSrcdocEntry) { + *aIsSrcdocEntry = mInfo->mSrcdocData.isSome(); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetSrcdocData(nsAString& aSrcdocData) { + aSrcdocData = mInfo->mSrcdocData.valueOr(EmptyString()); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetSrcdocData(const nsAString& aSrcdocData) { + mInfo->mSrcdocData = Some(nsString(aSrcdocData)); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetBaseURI(nsIURI** aBaseURI) { + nsCOMPtr<nsIURI> baseURI = mInfo->mBaseURI; + baseURI.forget(aBaseURI); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetBaseURI(nsIURI* aBaseURI) { + mInfo->mBaseURI = aBaseURI; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetScrollRestorationIsManual( + bool* aScrollRestorationIsManual) { + *aScrollRestorationIsManual = mInfo->mScrollRestorationIsManual; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetScrollRestorationIsManual( + bool aScrollRestorationIsManual) { + mInfo->mScrollRestorationIsManual = aScrollRestorationIsManual; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetLoadedInThisProcess(bool* aLoadedInThisProcess) { + // FIXME + //*aLoadedInThisProcess = mInfo->mLoadedInThisProcess; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetShistory(nsISHistory** aShistory) { + nsCOMPtr<nsISHistory> sHistory = do_QueryReferent(SharedInfo()->mSHistory); + sHistory.forget(aShistory); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetShistory(nsISHistory* aShistory) { + nsWeakPtr shistory = do_GetWeakReference(aShistory); + // mSHistory can not be changed once it's set + MOZ_ASSERT(!SharedInfo()->mSHistory || (SharedInfo()->mSHistory == shistory)); + SharedInfo()->mSHistory = shistory; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetLastTouched(uint32_t* aLastTouched) { + *aLastTouched = SharedInfo()->mLastTouched; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetLastTouched(uint32_t aLastTouched) { + SharedInfo()->mLastTouched = aLastTouched; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetChildCount(int32_t* aChildCount) { + *aChildCount = mChildren.Length(); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetPersist(bool* aPersist) { + *aPersist = mInfo->mPersist; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetPersist(bool aPersist) { + mInfo->mPersist = aPersist; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetScrollPosition(int32_t* aX, int32_t* aY) { + *aX = mInfo->mScrollPositionX; + *aY = mInfo->mScrollPositionY; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetScrollPosition(int32_t aX, int32_t aY) { + mInfo->mScrollPositionX = aX; + mInfo->mScrollPositionY = aY; + return NS_OK; +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::GetViewerBounds(nsIntRect& bounds) { + bounds = SharedInfo()->mViewerBounds; +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::SetViewerBounds(const nsIntRect& bounds) { + SharedInfo()->mViewerBounds = bounds; +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::AddChildShell(nsIDocShellTreeItem* shell) { + MOZ_CRASH("This lives in the child process"); +} + +NS_IMETHODIMP +SessionHistoryEntry::ChildShellAt(int32_t index, + nsIDocShellTreeItem** _retval) { + MOZ_CRASH("This lives in the child process"); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::ClearChildShells() { + MOZ_CRASH("This lives in the child process"); +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::SyncPresentationState() { + MOZ_CRASH("This lives in the child process"); +} + +NS_IMETHODIMP +SessionHistoryEntry::InitLayoutHistoryState( + nsILayoutHistoryState** aLayoutHistoryState) { + if (!SharedInfo()->mLayoutHistoryState) { + nsCOMPtr<nsILayoutHistoryState> historyState; + historyState = NS_NewLayoutHistoryState(); + SetLayoutHistoryState(historyState); + } + + return GetLayoutHistoryState(aLayoutHistoryState); +} + +NS_IMETHODIMP +SessionHistoryEntry::Create( + nsIURI* aURI, const nsAString& aTitle, nsIInputStream* aInputStream, + uint32_t aCacheKey, const nsACString& aContentType, + nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, const nsID& aDocshellID, + bool aDynamicCreation, nsIURI* aOriginalURI, nsIURI* aResultPrincipalURI, + nsIURI* aUnstrippedURI, bool aLoadReplace, nsIReferrerInfo* aReferrerInfo, + const nsAString& aSrcdoc, bool aSrcdocEntry, nsIURI* aBaseURI, + bool aSaveLayoutState, bool aExpired, bool aUserActivation) { + MOZ_CRASH("Might need to implement this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SessionHistoryEntry::Clone(nsISHEntry** aEntry) { + RefPtr<SessionHistoryEntry> entry = new SessionHistoryEntry(*this); + + // These are not copied for some reason, we're not sure why. + entry->mInfo->mLoadType = 0; + entry->mInfo->mScrollPositionX = 0; + entry->mInfo->mScrollPositionY = 0; + entry->mInfo->mScrollRestorationIsManual = false; + + entry->mInfo->mHasUserInteraction = false; + + entry.forget(aEntry); + + return NS_OK; +} + +NS_IMETHODIMP_(nsDocShellEditorData*) +SessionHistoryEntry::ForgetEditorData() { + MOZ_CRASH("This lives in the child process"); + return nullptr; +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::SetEditorData(nsDocShellEditorData* aData) { + NS_WARNING("This lives in the child process"); +} + +NS_IMETHODIMP_(bool) +SessionHistoryEntry::HasDetachedEditor() { + NS_WARNING("This lives in the child process"); + return false; +} + +NS_IMETHODIMP_(bool) +SessionHistoryEntry::IsDynamicallyAdded() { + return SharedInfo()->mDynamicallyCreated; +} + +void SessionHistoryEntry::SetWireframe(const Maybe<Wireframe>& aWireframe) { + mWireframe = aWireframe; +} + +void SessionHistoryEntry::SetIsDynamicallyAdded(bool aDynamic) { + MOZ_ASSERT_IF(SharedInfo()->mDynamicallyCreated, aDynamic); + SharedInfo()->mDynamicallyCreated = aDynamic; +} + +NS_IMETHODIMP +SessionHistoryEntry::HasDynamicallyAddedChild(bool* aHasDynamicallyAddedChild) { + for (const auto& child : mChildren) { + if (child && child->IsDynamicallyAdded()) { + *aHasDynamicallyAddedChild = true; + return NS_OK; + } + } + *aHasDynamicallyAddedChild = false; + return NS_OK; +} + +NS_IMETHODIMP_(bool) +SessionHistoryEntry::HasBFCacheEntry(SHEntrySharedParentState* aEntry) { + return SharedInfo() == aEntry; +} + +NS_IMETHODIMP +SessionHistoryEntry::AdoptBFCacheEntry(nsISHEntry* aEntry) { + nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aEntry); + NS_ENSURE_STATE(she && she->mInfo->mSharedState.Get()); + + mInfo->mSharedState = + static_cast<SessionHistoryEntry*>(aEntry)->mInfo->mSharedState; + + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::AbandonBFCacheEntry() { + MOZ_CRASH("This lives in the child process"); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +SessionHistoryEntry::SharesDocumentWith(nsISHEntry* aEntry, + bool* aSharesDocumentWith) { + SessionHistoryEntry* entry = static_cast<SessionHistoryEntry*>(aEntry); + + MOZ_ASSERT_IF(entry->SharedInfo() != SharedInfo(), + entry->SharedInfo()->GetId() != SharedInfo()->GetId()); + + *aSharesDocumentWith = entry->SharedInfo() == SharedInfo(); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetLoadTypeAsHistory() { + mInfo->mLoadType = LOAD_HISTORY; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::AddChild(nsISHEntry* aChild, int32_t aOffset, + bool aUseRemoteSubframes) { + nsCOMPtr<SessionHistoryEntry> child = do_QueryInterface(aChild); + MOZ_ASSERT_IF(aChild, child); + AddChild(child, aOffset, aUseRemoteSubframes); + + return NS_OK; +} + +void SessionHistoryEntry::AddChild(SessionHistoryEntry* aChild, int32_t aOffset, + bool aUseRemoteSubframes) { + if (aChild) { + aChild->SetParent(this); + } + + if (aOffset < 0) { + mChildren.AppendElement(aChild); + return; + } + + // + // Bug 52670: Ensure children are added in order. + // + // Later frames in the child list may load faster and get appended + // before earlier frames, causing session history to be scrambled. + // By growing the list here, they are added to the right position. + + int32_t length = mChildren.Length(); + + // Assert that aOffset will not be so high as to grow us a lot. + NS_ASSERTION(aOffset < length + 1023, "Large frames array!\n"); + + // If the new child is dynamically added, try to add it to aOffset, but if + // there are non-dynamically added children, the child must be after those. + if (aChild && aChild->IsDynamicallyAdded()) { + int32_t lastNonDyn = aOffset - 1; + for (int32_t i = aOffset; i < length; ++i) { + SessionHistoryEntry* entry = mChildren[i]; + if (entry) { + if (entry->IsDynamicallyAdded()) { + break; + } + + lastNonDyn = i; + } + } + + // If aOffset is larger than Length(), we must first truncate the array. + if (aOffset > length) { + mChildren.SetLength(aOffset); + } + + mChildren.InsertElementAt(lastNonDyn + 1, aChild); + + return; + } + + // If the new child isn't dynamically added, it should be set to aOffset. + // If there are dynamically added children before that, those must be moved + // to be after aOffset. + if (length > 0) { + int32_t start = std::min(length - 1, aOffset); + int32_t dynEntryIndex = -1; + DebugOnly<SessionHistoryEntry*> dynEntry = nullptr; + for (int32_t i = start; i >= 0; --i) { + SessionHistoryEntry* entry = mChildren[i]; + if (entry) { + if (!entry->IsDynamicallyAdded()) { + break; + } + + dynEntryIndex = i; + dynEntry = entry; + } + } + + if (dynEntryIndex >= 0) { + mChildren.InsertElementsAt(dynEntryIndex, aOffset - dynEntryIndex + 1); + NS_ASSERTION(mChildren[aOffset + 1] == dynEntry, "Whaat?"); + } + } + + // Make sure there isn't anything at aOffset. + if ((uint32_t)aOffset < mChildren.Length()) { + SessionHistoryEntry* oldChild = mChildren[aOffset]; + if (oldChild && oldChild != aChild) { + // Under Fission, this can happen when a network-created iframe starts + // out in-process, moves out-of-process, and then switches back. At that + // point, we'll create a new network-created DocShell at the same index + // where we already have an entry for the original network-created + // DocShell. + // + // This should ideally stop being an issue once the Fission-aware + // session history rewrite is complete. + NS_ASSERTION( + aUseRemoteSubframes || NS_IsAboutBlank(oldChild->Info().GetURI()), + "Adding a child where we already have a child? This may misbehave"); + oldChild->SetParent(nullptr); + } + } else { + mChildren.SetLength(aOffset + 1); + } + + mChildren.ReplaceElementAt(aOffset, aChild); +} + +NS_IMETHODIMP +SessionHistoryEntry::RemoveChild(nsISHEntry* aChild) { + NS_ENSURE_TRUE(aChild, NS_ERROR_FAILURE); + + nsCOMPtr<SessionHistoryEntry> child = do_QueryInterface(aChild); + MOZ_ASSERT(child); + RemoveChild(child); + + return NS_OK; +} + +void SessionHistoryEntry::RemoveChild(SessionHistoryEntry* aChild) { + bool childRemoved = false; + if (aChild->IsDynamicallyAdded()) { + childRemoved = mChildren.RemoveElement(aChild); + } else { + int32_t index = mChildren.IndexOf(aChild); + if (index >= 0) { + // Other alive non-dynamic child docshells still keep mChildOffset, + // so we don't want to change the indices here. + mChildren.ReplaceElementAt(index, nullptr); + childRemoved = true; + } + } + + if (childRemoved) { + aChild->SetParent(nullptr); + + // reduce the child count, i.e. remove empty children at the end + for (int32_t i = mChildren.Length() - 1; i >= 0 && !mChildren[i]; --i) { + mChildren.RemoveElementAt(i); + } + } +} + +NS_IMETHODIMP +SessionHistoryEntry::GetChildAt(int32_t aIndex, nsISHEntry** aChild) { + nsCOMPtr<nsISHEntry> child = mChildren.SafeElementAt(aIndex); + child.forget(aChild); + return NS_OK; +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::GetChildSHEntryIfHasNoDynamicallyAddedChild( + int32_t aChildOffset, nsISHEntry** aChild) { + *aChild = nullptr; + + bool dynamicallyAddedChild = false; + HasDynamicallyAddedChild(&dynamicallyAddedChild); + if (dynamicallyAddedChild) { + return; + } + + // If the user did a shift-reload on this frameset page, + // we don't want to load the subframes from history. + if (IsForceReloadType(mInfo->mLoadType) || mInfo->mLoadType == LOAD_REFRESH) { + return; + } + + /* Before looking for the subframe's url, check + * the expiration status of the parent. If the parent + * has expired from cache, then subframes will not be + * loaded from history in certain situations. + * If the user pressed reload and the parent frame has expired + * from cache, we do not want to load the child frame from history. + */ + if (SharedInfo()->mExpired && (mInfo->mLoadType == LOAD_RELOAD_NORMAL)) { + // The parent has expired. Return null. + *aChild = nullptr; + return; + } + // Get the child subframe from session history. + GetChildAt(aChildOffset, aChild); + if (*aChild) { + // Set the parent's Load Type on the child + (*aChild)->SetLoadType(mInfo->mLoadType); + } +} + +NS_IMETHODIMP +SessionHistoryEntry::ReplaceChild(nsISHEntry* aNewChild) { + NS_ENSURE_STATE(aNewChild); + + nsCOMPtr<SessionHistoryEntry> newChild = do_QueryInterface(aNewChild); + MOZ_ASSERT(newChild); + return ReplaceChild(newChild) ? NS_OK : NS_ERROR_FAILURE; +} + +bool SessionHistoryEntry::ReplaceChild(SessionHistoryEntry* aNewChild) { + const nsID& docshellID = aNewChild->DocshellID(); + + for (uint32_t i = 0; i < mChildren.Length(); ++i) { + if (mChildren[i] && docshellID == mChildren[i]->DocshellID()) { + mChildren[i]->SetParent(nullptr); + mChildren.ReplaceElementAt(i, aNewChild); + aNewChild->SetParent(this); + + return true; + } + } + + return false; +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::ClearEntry() { + int32_t childCount = GetChildCount(); + // Remove all children of this entry + for (int32_t i = childCount; i > 0; --i) { + nsCOMPtr<nsISHEntry> child; + GetChildAt(i - 1, getter_AddRefs(child)); + RemoveChild(child); + } +} + +NS_IMETHODIMP +SessionHistoryEntry::CreateLoadInfo(nsDocShellLoadState** aLoadState) { + NS_WARNING("We shouldn't be calling this!"); + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetBfcacheID(uint64_t* aBfcacheID) { + *aBfcacheID = SharedInfo()->mId; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::GetWireframe(JSContext* aCx, + JS::MutableHandle<JS::Value> aOut) { + if (mWireframe.isNothing()) { + aOut.set(JS::NullValue()); + } else if (NS_WARN_IF(!mWireframe->ToObjectInternal(aCx, aOut))) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetWireframe(JSContext* aCx, JS::Handle<JS::Value> aArg) { + if (aArg.isNullOrUndefined()) { + mWireframe = Nothing(); + return NS_OK; + } + + Wireframe wireframe; + if (aArg.isObject() && wireframe.Init(aCx, aArg)) { + mWireframe = Some(std::move(wireframe)); + return NS_OK; + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP_(void) +SessionHistoryEntry::SyncTreesForSubframeNavigation( + nsISHEntry* aEntry, mozilla::dom::BrowsingContext* aTopBC, + mozilla::dom::BrowsingContext* aIgnoreBC) { + // XXX Keep this in sync with nsSHEntry::SyncTreesForSubframeNavigation. + // + // We need to sync up the browsing context and session history trees for + // subframe navigation. If the load was in a subframe, we forward up to + // the top browsing context, which will then recursively sync up all browsing + // contexts to their corresponding entries in the new session history tree. If + // we don't do this, then we can cache a content viewer on the wrong cloned + // entry, and subsequently restore it at the wrong time. + nsCOMPtr<nsISHEntry> newRootEntry = nsSHistory::GetRootSHEntry(aEntry); + if (newRootEntry) { + // newRootEntry is now the new root entry. + // Find the old root entry as well. + + // Need a strong ref. on |oldRootEntry| so it isn't destroyed when + // SetChildHistoryEntry() does SwapHistoryEntries() (bug 304639). + nsCOMPtr<nsISHEntry> oldRootEntry = nsSHistory::GetRootSHEntry(this); + + if (oldRootEntry) { + nsSHistory::SwapEntriesData data = {aIgnoreBC, newRootEntry, nullptr}; + nsSHistory::SetChildHistoryEntry(oldRootEntry, aTopBC, 0, &data); + } + } +} + +void SessionHistoryEntry::ReplaceWith(const SessionHistoryEntry& aSource) { + mInfo = MakeUnique<SessionHistoryInfo>(*aSource.mInfo); + mChildren.Clear(); +} + +SHEntrySharedParentState* SessionHistoryEntry::SharedInfo() const { + return static_cast<SHEntrySharedParentState*>(mInfo->mSharedState.Get()); +} + +void SessionHistoryEntry::SetFrameLoader(nsFrameLoader* aFrameLoader) { + MOZ_DIAGNOSTIC_ASSERT(!aFrameLoader || !SharedInfo()->mFrameLoader); + // If the pref is disabled, we still allow evicting the existing entries. + MOZ_RELEASE_ASSERT(!aFrameLoader || mozilla::BFCacheInParent()); + SharedInfo()->SetFrameLoader(aFrameLoader); + if (aFrameLoader) { + if (BrowsingContext* bc = aFrameLoader->GetMaybePendingBrowsingContext()) { + bc->PreOrderWalk([&](BrowsingContext* aContext) { + if (BrowserParent* bp = aContext->Canonical()->GetBrowserParent()) { + bp->Deactivated(); + } + }); + } + + // When a new frameloader is stored, try to evict some older + // frameloaders. Non-SHIP session history has a similar call in + // nsDocumentViewer::Show. + nsCOMPtr<nsISHistory> shistory; + GetShistory(getter_AddRefs(shistory)); + if (shistory) { + int32_t index = 0; + shistory->GetIndex(&index); + shistory->EvictOutOfRangeContentViewers(index); + } + } +} + +nsFrameLoader* SessionHistoryEntry::GetFrameLoader() { + return SharedInfo()->mFrameLoader; +} + +void SessionHistoryEntry::SetInfo(SessionHistoryInfo* aInfo) { + // FIXME Assert that we're not changing shared state! + mInfo = MakeUnique<SessionHistoryInfo>(*aInfo); +} + +} // namespace dom + +namespace ipc { + +void IPDLParamTraits<dom::SessionHistoryInfo>::Write( + IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::SessionHistoryInfo& aParam) { + nsCOMPtr<nsIInputStream> postData = aParam.GetPostData(); + + Maybe<std::tuple<uint32_t, dom::ClonedMessageData>> stateData; + if (aParam.mStateData) { + stateData.emplace(); + // FIXME: We should fail more aggressively if this fails, as currently we'll + // just early return and the deserialization will break. + NS_ENSURE_SUCCESS_VOID( + aParam.mStateData->GetFormatVersion(&std::get<0>(*stateData))); + NS_ENSURE_TRUE_VOID( + aParam.mStateData->BuildClonedMessageData(std::get<1>(*stateData))); + } + + WriteIPDLParam(aWriter, aActor, aParam.mURI); + WriteIPDLParam(aWriter, aActor, aParam.mOriginalURI); + WriteIPDLParam(aWriter, aActor, aParam.mResultPrincipalURI); + WriteIPDLParam(aWriter, aActor, aParam.mUnstrippedURI); + WriteIPDLParam(aWriter, aActor, aParam.mReferrerInfo); + WriteIPDLParam(aWriter, aActor, aParam.mTitle); + WriteIPDLParam(aWriter, aActor, aParam.mName); + WriteIPDLParam(aWriter, aActor, postData); + WriteIPDLParam(aWriter, aActor, aParam.mLoadType); + WriteIPDLParam(aWriter, aActor, aParam.mScrollPositionX); + WriteIPDLParam(aWriter, aActor, aParam.mScrollPositionY); + WriteIPDLParam(aWriter, aActor, stateData); + WriteIPDLParam(aWriter, aActor, aParam.mSrcdocData); + WriteIPDLParam(aWriter, aActor, aParam.mBaseURI); + WriteIPDLParam(aWriter, aActor, aParam.mLoadReplace); + WriteIPDLParam(aWriter, aActor, aParam.mURIWasModified); + WriteIPDLParam(aWriter, aActor, aParam.mScrollRestorationIsManual); + WriteIPDLParam(aWriter, aActor, aParam.mPersist); + WriteIPDLParam(aWriter, aActor, aParam.mHasUserInteraction); + WriteIPDLParam(aWriter, aActor, aParam.mHasUserActivation); + WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mId); + WriteIPDLParam(aWriter, aActor, + aParam.mSharedState.Get()->mTriggeringPrincipal); + WriteIPDLParam(aWriter, aActor, + aParam.mSharedState.Get()->mPrincipalToInherit); + WriteIPDLParam(aWriter, aActor, + aParam.mSharedState.Get()->mPartitionedPrincipalToInherit); + WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mCsp); + WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mContentType); + WriteIPDLParam(aWriter, aActor, + aParam.mSharedState.Get()->mLayoutHistoryState); + WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mCacheKey); + WriteIPDLParam(aWriter, aActor, + aParam.mSharedState.Get()->mIsFrameNavigation); + WriteIPDLParam(aWriter, aActor, aParam.mSharedState.Get()->mSaveLayoutState); +} + +bool IPDLParamTraits<dom::SessionHistoryInfo>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + dom::SessionHistoryInfo* aResult) { + Maybe<std::tuple<uint32_t, dom::ClonedMessageData>> stateData; + uint64_t sharedId; + if (!ReadIPDLParam(aReader, aActor, &aResult->mURI) || + !ReadIPDLParam(aReader, aActor, &aResult->mOriginalURI) || + !ReadIPDLParam(aReader, aActor, &aResult->mResultPrincipalURI) || + !ReadIPDLParam(aReader, aActor, &aResult->mUnstrippedURI) || + !ReadIPDLParam(aReader, aActor, &aResult->mReferrerInfo) || + !ReadIPDLParam(aReader, aActor, &aResult->mTitle) || + !ReadIPDLParam(aReader, aActor, &aResult->mName) || + !ReadIPDLParam(aReader, aActor, &aResult->mPostData) || + !ReadIPDLParam(aReader, aActor, &aResult->mLoadType) || + !ReadIPDLParam(aReader, aActor, &aResult->mScrollPositionX) || + !ReadIPDLParam(aReader, aActor, &aResult->mScrollPositionY) || + !ReadIPDLParam(aReader, aActor, &stateData) || + !ReadIPDLParam(aReader, aActor, &aResult->mSrcdocData) || + !ReadIPDLParam(aReader, aActor, &aResult->mBaseURI) || + !ReadIPDLParam(aReader, aActor, &aResult->mLoadReplace) || + !ReadIPDLParam(aReader, aActor, &aResult->mURIWasModified) || + !ReadIPDLParam(aReader, aActor, &aResult->mScrollRestorationIsManual) || + !ReadIPDLParam(aReader, aActor, &aResult->mPersist) || + !ReadIPDLParam(aReader, aActor, &aResult->mHasUserInteraction) || + !ReadIPDLParam(aReader, aActor, &aResult->mHasUserActivation) || + !ReadIPDLParam(aReader, aActor, &sharedId)) { + aActor->FatalError("Error reading fields for SessionHistoryInfo"); + return false; + } + + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + nsCOMPtr<nsIPrincipal> principalToInherit; + nsCOMPtr<nsIPrincipal> partitionedPrincipalToInherit; + nsCOMPtr<nsIContentSecurityPolicy> csp; + nsCString contentType; + if (!ReadIPDLParam(aReader, aActor, &triggeringPrincipal) || + !ReadIPDLParam(aReader, aActor, &principalToInherit) || + !ReadIPDLParam(aReader, aActor, &partitionedPrincipalToInherit) || + !ReadIPDLParam(aReader, aActor, &csp) || + !ReadIPDLParam(aReader, aActor, &contentType)) { + aActor->FatalError("Error reading fields for SessionHistoryInfo"); + return false; + } + + // We should always see a cloneable input stream passed to SessionHistoryInfo. + // This is because it will be cloneable when first read in the parent process + // from the nsHttpChannel (which forces streams to be cloneable), and future + // streams in content will be wrapped in + // nsMIMEInputStream(RemoteLazyInputStream) which is also cloneable. + if (aResult->mPostData && !NS_InputStreamIsCloneable(aResult->mPostData)) { + aActor->FatalError( + "Unexpected non-cloneable postData for SessionHistoryInfo"); + return false; + } + + dom::SHEntrySharedParentState* sharedState = nullptr; + if (XRE_IsParentProcess()) { + sharedState = dom::SHEntrySharedParentState::Lookup(sharedId); + } + + if (sharedState) { + aResult->mSharedState.Set(sharedState); + + MOZ_ASSERT(triggeringPrincipal + ? triggeringPrincipal->Equals( + aResult->mSharedState.Get()->mTriggeringPrincipal) + : !aResult->mSharedState.Get()->mTriggeringPrincipal, + "We don't expect this to change!"); + MOZ_ASSERT(principalToInherit + ? principalToInherit->Equals( + aResult->mSharedState.Get()->mPrincipalToInherit) + : !aResult->mSharedState.Get()->mPrincipalToInherit, + "We don't expect this to change!"); + MOZ_ASSERT( + partitionedPrincipalToInherit + ? partitionedPrincipalToInherit->Equals( + aResult->mSharedState.Get()->mPartitionedPrincipalToInherit) + : !aResult->mSharedState.Get()->mPartitionedPrincipalToInherit, + "We don't expect this to change!"); + MOZ_ASSERT( + csp ? nsCSPContext::Equals(csp, aResult->mSharedState.Get()->mCsp) + : !aResult->mSharedState.Get()->mCsp, + "We don't expect this to change!"); + MOZ_ASSERT(contentType.Equals(aResult->mSharedState.Get()->mContentType), + "We don't expect this to change!"); + } else { + aResult->mSharedState.ChangeId(sharedId); + aResult->mSharedState.Get()->mTriggeringPrincipal = + triggeringPrincipal.forget(); + aResult->mSharedState.Get()->mPrincipalToInherit = + principalToInherit.forget(); + aResult->mSharedState.Get()->mPartitionedPrincipalToInherit = + partitionedPrincipalToInherit.forget(); + aResult->mSharedState.Get()->mCsp = csp.forget(); + aResult->mSharedState.Get()->mContentType = contentType; + } + + if (!ReadIPDLParam(aReader, aActor, + &aResult->mSharedState.Get()->mLayoutHistoryState) || + !ReadIPDLParam(aReader, aActor, + &aResult->mSharedState.Get()->mCacheKey) || + !ReadIPDLParam(aReader, aActor, + &aResult->mSharedState.Get()->mIsFrameNavigation) || + !ReadIPDLParam(aReader, aActor, + &aResult->mSharedState.Get()->mSaveLayoutState)) { + aActor->FatalError("Error reading fields for SessionHistoryInfo"); + return false; + } + + if (stateData.isSome()) { + uint32_t version = std::get<0>(*stateData); + aResult->mStateData = new nsStructuredCloneContainer(version); + aResult->mStateData->StealFromClonedMessageData(std::get<1>(*stateData)); + } + MOZ_ASSERT_IF(stateData.isNothing(), !aResult->mStateData); + return true; +} + +void IPDLParamTraits<dom::LoadingSessionHistoryInfo>::Write( + IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::LoadingSessionHistoryInfo& aParam) { + WriteIPDLParam(aWriter, aActor, aParam.mInfo); + WriteIPDLParam(aWriter, aActor, aParam.mLoadId); + WriteIPDLParam(aWriter, aActor, aParam.mLoadIsFromSessionHistory); + WriteIPDLParam(aWriter, aActor, aParam.mOffset); + WriteIPDLParam(aWriter, aActor, aParam.mLoadingCurrentEntry); + WriteIPDLParam(aWriter, aActor, aParam.mForceMaybeResetName); +} + +bool IPDLParamTraits<dom::LoadingSessionHistoryInfo>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + dom::LoadingSessionHistoryInfo* aResult) { + if (!ReadIPDLParam(aReader, aActor, &aResult->mInfo) || + !ReadIPDLParam(aReader, aActor, &aResult->mLoadId) || + !ReadIPDLParam(aReader, aActor, &aResult->mLoadIsFromSessionHistory) || + !ReadIPDLParam(aReader, aActor, &aResult->mOffset) || + !ReadIPDLParam(aReader, aActor, &aResult->mLoadingCurrentEntry) || + !ReadIPDLParam(aReader, aActor, &aResult->mForceMaybeResetName)) { + aActor->FatalError("Error reading fields for LoadingSessionHistoryInfo"); + return false; + } + + return true; +} + +void IPDLParamTraits<nsILayoutHistoryState*>::Write( + IPC::MessageWriter* aWriter, IProtocol* aActor, + nsILayoutHistoryState* aParam) { + if (aParam) { + WriteIPDLParam(aWriter, aActor, true); + bool scrollPositionOnly = false; + nsTArray<nsCString> keys; + nsTArray<mozilla::PresState> states; + aParam->GetContents(&scrollPositionOnly, keys, states); + WriteIPDLParam(aWriter, aActor, scrollPositionOnly); + WriteIPDLParam(aWriter, aActor, keys); + WriteIPDLParam(aWriter, aActor, states); + } else { + WriteIPDLParam(aWriter, aActor, false); + } +} + +bool IPDLParamTraits<nsILayoutHistoryState*>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + RefPtr<nsILayoutHistoryState>* aResult) { + bool hasLayoutHistoryState = false; + if (!ReadIPDLParam(aReader, aActor, &hasLayoutHistoryState)) { + aActor->FatalError("Error reading fields for nsILayoutHistoryState"); + return false; + } + + if (hasLayoutHistoryState) { + bool scrollPositionOnly = false; + nsTArray<nsCString> keys; + nsTArray<mozilla::PresState> states; + if (!ReadIPDLParam(aReader, aActor, &scrollPositionOnly) || + !ReadIPDLParam(aReader, aActor, &keys) || + !ReadIPDLParam(aReader, aActor, &states)) { + aActor->FatalError("Error reading fields for nsILayoutHistoryState"); + } + + if (keys.Length() != states.Length()) { + aActor->FatalError("Error reading fields for nsILayoutHistoryState"); + return false; + } + + *aResult = NS_NewLayoutHistoryState(); + (*aResult)->SetScrollPositionOnly(scrollPositionOnly); + for (uint32_t i = 0; i < keys.Length(); ++i) { + PresState& state = states[i]; + UniquePtr<PresState> newState = MakeUnique<PresState>(state); + (*aResult)->AddState(keys[i], std::move(newState)); + } + } + return true; +} + +void IPDLParamTraits<mozilla::dom::Wireframe>::Write( + IPC::MessageWriter* aWriter, IProtocol* aActor, + const mozilla::dom::Wireframe& aParam) { + WriteParam(aWriter, aParam.mCanvasBackground); + WriteParam(aWriter, aParam.mRects); +} + +bool IPDLParamTraits<mozilla::dom::Wireframe>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + mozilla::dom::Wireframe* aResult) { + return ReadParam(aReader, &aResult->mCanvasBackground) && + ReadParam(aReader, &aResult->mRects); +} + +} // namespace ipc +} // namespace mozilla + +namespace IPC { +// Allow sending mozilla::dom::WireframeRectType enums over IPC. +template <> +struct ParamTraits<mozilla::dom::WireframeRectType> + : public ContiguousEnumSerializer< + mozilla::dom::WireframeRectType, + mozilla::dom::WireframeRectType::Image, + mozilla::dom::WireframeRectType::EndGuard_> {}; + +template <> +struct ParamTraits<mozilla::dom::WireframeTaggedRect> { + static void Write(MessageWriter* aWriter, + const mozilla::dom::WireframeTaggedRect& aParam); + static bool Read(MessageReader* aReader, + mozilla::dom::WireframeTaggedRect* aResult); +}; + +void ParamTraits<mozilla::dom::WireframeTaggedRect>::Write( + MessageWriter* aWriter, const mozilla::dom::WireframeTaggedRect& aParam) { + WriteParam(aWriter, aParam.mColor); + WriteParam(aWriter, aParam.mType); + WriteParam(aWriter, aParam.mX); + WriteParam(aWriter, aParam.mY); + WriteParam(aWriter, aParam.mWidth); + WriteParam(aWriter, aParam.mHeight); +} + +bool ParamTraits<mozilla::dom::WireframeTaggedRect>::Read( + IPC::MessageReader* aReader, mozilla::dom::WireframeTaggedRect* aResult) { + return ReadParam(aReader, &aResult->mColor) && + ReadParam(aReader, &aResult->mType) && + ReadParam(aReader, &aResult->mX) && ReadParam(aReader, &aResult->mY) && + ReadParam(aReader, &aResult->mWidth) && + ReadParam(aReader, &aResult->mHeight); +} +} // namespace IPC diff --git a/docshell/shistory/SessionHistoryEntry.h b/docshell/shistory/SessionHistoryEntry.h new file mode 100644 index 0000000000..8eeb5ad2a9 --- /dev/null +++ b/docshell/shistory/SessionHistoryEntry.h @@ -0,0 +1,510 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_SessionHistoryEntry_h +#define mozilla_dom_SessionHistoryEntry_h + +#include "mozilla/dom/DocumentBinding.h" +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" +#include "nsILayoutHistoryState.h" +#include "nsISHEntry.h" +#include "nsSHEntryShared.h" +#include "nsStructuredCloneContainer.h" +#include "nsTHashMap.h" +#include "nsWeakReference.h" + +class nsDocShellLoadState; +class nsIChannel; +class nsIInputStream; +class nsIReferrerInfo; +class nsISHistory; +class nsIURI; + +namespace mozilla::ipc { +template <typename P> +struct IPDLParamTraits; +} + +namespace mozilla { +namespace dom { + +struct LoadingSessionHistoryInfo; +class SessionHistoryEntry; +class SHEntrySharedParentState; + +// SessionHistoryInfo stores session history data for a load. It can be sent +// over IPC and is used in both the parent and the child processes. +class SessionHistoryInfo { + public: + SessionHistoryInfo() = default; + SessionHistoryInfo(const SessionHistoryInfo& aInfo) = default; + SessionHistoryInfo(nsDocShellLoadState* aLoadState, nsIChannel* aChannel); + SessionHistoryInfo(const SessionHistoryInfo& aSharedStateFrom, nsIURI* aURI); + SessionHistoryInfo(nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, + const nsACString& aContentType); + SessionHistoryInfo(nsIChannel* aChannel, uint32_t aLoadType, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp); + + void Reset(nsIURI* aURI, const nsID& aDocShellID, bool aDynamicCreation, + nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, const nsACString& aContentType); + + bool operator==(const SessionHistoryInfo& aInfo) const { + return false; // FIXME + } + + nsIURI* GetURI() const { return mURI; } + void SetURI(nsIURI* aURI) { mURI = aURI; } + + nsIURI* GetOriginalURI() const { return mOriginalURI; } + void SetOriginalURI(nsIURI* aOriginalURI) { mOriginalURI = aOriginalURI; } + + nsIURI* GetUnstrippedURI() const { return mUnstrippedURI; } + void SetUnstrippedURI(nsIURI* aUnstrippedURI) { + mUnstrippedURI = aUnstrippedURI; + } + + nsIURI* GetResultPrincipalURI() const { return mResultPrincipalURI; } + void SetResultPrincipalURI(nsIURI* aResultPrincipalURI) { + mResultPrincipalURI = aResultPrincipalURI; + } + + nsIReferrerInfo* GetReferrerInfo() { return mReferrerInfo; } + void SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + mReferrerInfo = aReferrerInfo; + } + + bool HasPostData() const { return mPostData; } + already_AddRefed<nsIInputStream> GetPostData() const; + void SetPostData(nsIInputStream* aPostData); + + void GetScrollPosition(int32_t* aScrollPositionX, int32_t* aScrollPositionY) { + *aScrollPositionX = mScrollPositionX; + *aScrollPositionY = mScrollPositionY; + } + + void SetScrollPosition(int32_t aScrollPositionX, int32_t aScrollPositionY) { + mScrollPositionX = aScrollPositionX; + mScrollPositionY = aScrollPositionY; + } + + bool GetScrollRestorationIsManual() const { + return mScrollRestorationIsManual; + } + const nsAString& GetTitle() { return mTitle; } + void SetTitle(const nsAString& aTitle) { + mTitle = aTitle; + MaybeUpdateTitleFromURI(); + } + + const nsAString& GetName() { return mName; } + void SetName(const nsAString& aName) { mName = aName; } + + void SetScrollRestorationIsManual(bool aIsManual) { + mScrollRestorationIsManual = aIsManual; + } + + nsStructuredCloneContainer* GetStateData() const { return mStateData; } + void SetStateData(nsStructuredCloneContainer* aStateData) { + mStateData = aStateData; + } + + void SetLoadReplace(bool aLoadReplace) { mLoadReplace = aLoadReplace; } + + void SetURIWasModified(bool aURIWasModified) { + mURIWasModified = aURIWasModified; + } + bool GetURIWasModified() const { return mURIWasModified; } + + void SetHasUserInteraction(bool aHasUserInteraction) { + mHasUserInteraction = aHasUserInteraction; + } + bool GetHasUserInteraction() const { return mHasUserInteraction; } + + uint64_t SharedId() const; + + nsILayoutHistoryState* GetLayoutHistoryState(); + void SetLayoutHistoryState(nsILayoutHistoryState* aState); + + nsIPrincipal* GetTriggeringPrincipal() const; + + nsIPrincipal* GetPrincipalToInherit() const; + + nsIPrincipal* GetPartitionedPrincipalToInherit() const; + + nsIContentSecurityPolicy* GetCsp() const; + + uint32_t GetCacheKey() const; + void SetCacheKey(uint32_t aCacheKey); + + bool IsSubFrame() const; + + bool SharesDocumentWith(const SessionHistoryInfo& aOther) const { + return SharedId() == aOther.SharedId(); + } + + void FillLoadInfo(nsDocShellLoadState& aLoadState) const; + + uint32_t LoadType() { return mLoadType; } + + void SetSaveLayoutStateFlag(bool aSaveLayoutStateFlag); + + bool GetPersist() const { return mPersist; } + + private: + friend class SessionHistoryEntry; + friend struct mozilla::ipc::IPDLParamTraits<SessionHistoryInfo>; + + void MaybeUpdateTitleFromURI(); + + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mOriginalURI; + nsCOMPtr<nsIURI> mResultPrincipalURI; + nsCOMPtr<nsIURI> mUnstrippedURI; + nsCOMPtr<nsIReferrerInfo> mReferrerInfo; + nsString mTitle; + nsString mName; + nsCOMPtr<nsIInputStream> mPostData; + uint32_t mLoadType = 0; + int32_t mScrollPositionX = 0; + int32_t mScrollPositionY = 0; + RefPtr<nsStructuredCloneContainer> mStateData; + Maybe<nsString> mSrcdocData; + nsCOMPtr<nsIURI> mBaseURI; + + bool mLoadReplace = false; + bool mURIWasModified = false; + bool mScrollRestorationIsManual = false; + bool mPersist = true; + bool mHasUserInteraction = false; + bool mHasUserActivation = false; + + union SharedState { + SharedState(); + explicit SharedState(const SharedState& aOther); + explicit SharedState(const Maybe<const SharedState&>& aOther); + ~SharedState(); + + SharedState& operator=(const SharedState& aOther); + + SHEntrySharedState* Get() const; + + void Set(SHEntrySharedParentState* aState) { mParent = aState; } + + void ChangeId(uint64_t aId); + + static SharedState Create(nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, + const nsACString& aContentType); + + private: + explicit SharedState(SHEntrySharedParentState* aParent) + : mParent(aParent) {} + explicit SharedState(UniquePtr<SHEntrySharedState>&& aChild) + : mChild(std::move(aChild)) {} + + void Init(); + void Init(const SharedState& aOther); + + // In the parent process this holds a strong reference to the refcounted + // SHEntrySharedParentState. In the child processes this holds an owning + // pointer to a SHEntrySharedState. + RefPtr<SHEntrySharedParentState> mParent; + UniquePtr<SHEntrySharedState> mChild; + }; + + SharedState mSharedState; +}; + +struct LoadingSessionHistoryInfo { + LoadingSessionHistoryInfo() = default; + explicit LoadingSessionHistoryInfo(SessionHistoryEntry* aEntry); + // Initializes mInfo using aEntry and otherwise copies the values from aInfo. + LoadingSessionHistoryInfo(SessionHistoryEntry* aEntry, + const LoadingSessionHistoryInfo* aInfo); + // For about:blank only. + explicit LoadingSessionHistoryInfo(const SessionHistoryInfo& aInfo); + + already_AddRefed<nsDocShellLoadState> CreateLoadInfo() const; + + SessionHistoryInfo mInfo; + + uint64_t mLoadId = 0; + + // The following three member variables are used to inform about a load from + // the session history. The session-history-in-child approach has just + // an nsISHEntry in the nsDocShellLoadState and access to the nsISHistory, + // but session-history-in-parent needs to pass needed information explicitly + // to the relevant child process. + bool mLoadIsFromSessionHistory = false; + // mOffset and mLoadingCurrentEntry are relevant only if + // mLoadIsFromSessionHistory is true. + int32_t mOffset = 0; + // If we're loading from the current entry we want to treat it as not a + // same-document navigation (see nsDocShell::IsSameDocumentNavigation). + bool mLoadingCurrentEntry = false; + // If mForceMaybeResetName.isSome() is true then the parent process has + // determined whether the BC's name should be cleared and stored in session + // history (see https://html.spec.whatwg.org/#history-traversal step 4.2). + // This is used when we're replacing the BC for BFCache in the parent. In + // other cases mForceMaybeResetName.isSome() will be false and the child + // process should be able to make that determination itself. + Maybe<bool> mForceMaybeResetName; +}; + +// HistoryEntryCounterForBrowsingContext is used to count the number of entries +// which are added to the session history for a particular browsing context. +// If a SessionHistoryEntry is cloned because of navigation in some other +// browsing context, that doesn't cause the counter value to be increased. +// The browsing context specific counter is needed to make it easier to +// synchronously update history.length value in a child process when +// an iframe is removed from DOM. +class HistoryEntryCounterForBrowsingContext { + public: + HistoryEntryCounterForBrowsingContext() + : mCounter(new RefCountedCounter()), mHasModified(false) { + ++(*this); + } + + HistoryEntryCounterForBrowsingContext( + const HistoryEntryCounterForBrowsingContext& aOther) + : mCounter(aOther.mCounter), mHasModified(false) {} + + HistoryEntryCounterForBrowsingContext( + HistoryEntryCounterForBrowsingContext&& aOther) = delete; + + ~HistoryEntryCounterForBrowsingContext() { + if (mHasModified) { + --(*mCounter); + } + } + + void CopyValueFrom(const HistoryEntryCounterForBrowsingContext& aOther) { + if (mHasModified) { + --(*mCounter); + } + mCounter = aOther.mCounter; + mHasModified = false; + } + + HistoryEntryCounterForBrowsingContext& operator=( + const HistoryEntryCounterForBrowsingContext& aOther) = delete; + + HistoryEntryCounterForBrowsingContext& operator++() { + mHasModified = true; + ++(*mCounter); + return *this; + } + + operator uint32_t() const { return *mCounter; } + + bool Modified() { return mHasModified; } + + void SetModified(bool aModified) { mHasModified = aModified; } + + void Reset() { + if (mHasModified) { + --(*mCounter); + } + mCounter = new RefCountedCounter(); + mHasModified = false; + } + + private: + class RefCountedCounter { + public: + NS_INLINE_DECL_REFCOUNTING( + mozilla::dom::HistoryEntryCounterForBrowsingContext::RefCountedCounter) + + RefCountedCounter& operator++() { + ++mCounter; + return *this; + } + + RefCountedCounter& operator--() { + --mCounter; + return *this; + } + + operator uint32_t() const { return mCounter; } + + private: + ~RefCountedCounter() = default; + + uint32_t mCounter = 0; + }; + + RefPtr<RefCountedCounter> mCounter; + bool mHasModified; +}; + +// SessionHistoryEntry is used to store session history data in the parent +// process. It holds a SessionHistoryInfo, some state shared amongst multiple +// SessionHistoryEntries, a parent and children. +#define NS_SESSIONHISTORYENTRY_IID \ + { \ + 0x5b66a244, 0x8cec, 0x4caa, { \ + 0xaa, 0x0a, 0x78, 0x92, 0xfd, 0x17, 0xa6, 0x67 \ + } \ + } + +class SessionHistoryEntry : public nsISHEntry, public nsSupportsWeakReference { + public: + SessionHistoryEntry(nsDocShellLoadState* aLoadState, nsIChannel* aChannel); + SessionHistoryEntry(); + explicit SessionHistoryEntry(SessionHistoryInfo* aInfo); + explicit SessionHistoryEntry(const SessionHistoryEntry& aEntry); + + NS_DECL_ISUPPORTS + NS_DECL_NSISHENTRY + NS_DECLARE_STATIC_IID_ACCESSOR(NS_SESSIONHISTORYENTRY_IID) + + bool IsInSessionHistory() { + SessionHistoryEntry* entry = this; + while (nsCOMPtr<SessionHistoryEntry> parent = + do_QueryReferent(entry->mParent)) { + entry = parent; + } + return entry->SharedInfo()->mSHistory && + entry->SharedInfo()->mSHistory->IsAlive(); + } + + void ReplaceWith(const SessionHistoryEntry& aSource); + + const SessionHistoryInfo& Info() const { return *mInfo; } + + SHEntrySharedParentState* SharedInfo() const; + + void SetFrameLoader(nsFrameLoader* aFrameLoader); + nsFrameLoader* GetFrameLoader(); + + void AddChild(SessionHistoryEntry* aChild, int32_t aOffset, + bool aUseRemoteSubframes); + void RemoveChild(SessionHistoryEntry* aChild); + // Finds the child with the same docshell ID as aNewChild, replaces it with + // aNewChild and returns true. If there is no child with the same docshell ID + // then it returns false. + bool ReplaceChild(SessionHistoryEntry* aNewChild); + + void SetInfo(SessionHistoryInfo* aInfo); + + bool ForInitialLoad() { return mForInitialLoad; } + void SetForInitialLoad(bool aForInitialLoad) { + mForInitialLoad = aForInitialLoad; + } + + const nsID& DocshellID() const; + + HistoryEntryCounterForBrowsingContext& BCHistoryLength() { + return mBCHistoryLength; + } + + void SetBCHistoryLength(HistoryEntryCounterForBrowsingContext& aCounter) { + mBCHistoryLength.CopyValueFrom(aCounter); + } + + void ClearBCHistoryLength() { mBCHistoryLength.Reset(); } + + void SetIsDynamicallyAdded(bool aDynamic); + + void SetWireframe(const Maybe<Wireframe>& aWireframe); + + struct LoadingEntry { + // A pointer to the entry being loaded. Will be cleared by the + // SessionHistoryEntry destructor, at latest. + SessionHistoryEntry* mEntry; + // Snapshot of the entry's SessionHistoryInfo when the load started, to be + // used for validation purposes only. + UniquePtr<SessionHistoryInfo> mInfoSnapshotForValidation; + }; + + // Get an entry based on LoadingSessionHistoryInfo's mLoadId. Parent process + // only. + static LoadingEntry* GetByLoadId(uint64_t aLoadId); + static void SetByLoadId(uint64_t aLoadId, SessionHistoryEntry* aEntry); + static void RemoveLoadId(uint64_t aLoadId); + + const nsTArray<RefPtr<SessionHistoryEntry>>& Children() { return mChildren; } + + private: + friend struct LoadingSessionHistoryInfo; + virtual ~SessionHistoryEntry(); + + UniquePtr<SessionHistoryInfo> mInfo; + nsWeakPtr mParent; + uint32_t mID; + nsTArray<RefPtr<SessionHistoryEntry>> mChildren; + Maybe<Wireframe> mWireframe; + + bool mForInitialLoad = false; + + HistoryEntryCounterForBrowsingContext mBCHistoryLength; + + static nsTHashMap<nsUint64HashKey, LoadingEntry>* sLoadIdToEntry; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(SessionHistoryEntry, NS_SESSIONHISTORYENTRY_IID) + +} // namespace dom + +namespace ipc { + +class IProtocol; + +// Allow sending SessionHistoryInfo objects over IPC. +template <> +struct IPDLParamTraits<dom::SessionHistoryInfo> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::SessionHistoryInfo& aParam); + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + dom::SessionHistoryInfo* aResult); +}; + +// Allow sending LoadingSessionHistoryInfo objects over IPC. +template <> +struct IPDLParamTraits<dom::LoadingSessionHistoryInfo> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const dom::LoadingSessionHistoryInfo& aParam); + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + dom::LoadingSessionHistoryInfo* aResult); +}; + +// Allow sending nsILayoutHistoryState objects over IPC. +template <> +struct IPDLParamTraits<nsILayoutHistoryState*> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + nsILayoutHistoryState* aParam); + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + RefPtr<nsILayoutHistoryState>* aResult); +}; + +// Allow sending dom::Wireframe objects over IPC. +template <> +struct IPDLParamTraits<mozilla::dom::Wireframe> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + const mozilla::dom::Wireframe& aParam); + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + mozilla::dom::Wireframe* aResult); +}; + +} // namespace ipc + +} // namespace mozilla + +inline nsISupports* ToSupports(mozilla::dom::SessionHistoryEntry* aEntry) { + return static_cast<nsISHEntry*>(aEntry); +} + +#endif /* mozilla_dom_SessionHistoryEntry_h */ diff --git a/docshell/shistory/moz.build b/docshell/shistory/moz.build new file mode 100644 index 0000000000..f3b86c207b --- /dev/null +++ b/docshell/shistory/moz.build @@ -0,0 +1,42 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + "nsIBFCacheEntry.idl", + "nsISHEntry.idl", + "nsISHistory.idl", + "nsISHistoryListener.idl", +] + +XPIDL_MODULE = "shistory" + +EXPORTS += [ + "nsSHEntry.h", + "nsSHEntryShared.h", + "nsSHistory.h", +] + +EXPORTS.mozilla.dom += [ + "ChildSHistory.h", + "SessionHistoryEntry.h", +] + +UNIFIED_SOURCES += [ + "ChildSHistory.cpp", + "nsSHEntry.cpp", + "nsSHEntryShared.cpp", + "nsSHistory.cpp", + "SessionHistoryEntry.cpp", +] + +LOCAL_INCLUDES += [ + "/docshell/base", + "/dom/base", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/docshell/shistory/nsIBFCacheEntry.idl b/docshell/shistory/nsIBFCacheEntry.idl new file mode 100644 index 0000000000..2e24c67e35 --- /dev/null +++ b/docshell/shistory/nsIBFCacheEntry.idl @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +/** + * This interface lets you evict a document from the back/forward cache. + */ +[scriptable, builtinclass, uuid(a576060e-c7df-4d81-aa8c-5b52bd6ad25d)] +interface nsIBFCacheEntry : nsISupports +{ + void RemoveFromBFCacheSync(); + void RemoveFromBFCacheAsync(); +}; diff --git a/docshell/shistory/nsISHEntry.idl b/docshell/shistory/nsISHEntry.idl new file mode 100644 index 0000000000..28161e8885 --- /dev/null +++ b/docshell/shistory/nsISHEntry.idl @@ -0,0 +1,476 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +/** + * The interface to nsISHentry. Each document or subframe in + * Session History will have a nsISHEntry associated with it which will + * hold all information required to recreate the document from history + */ + +#include "nsISupports.idl" + +interface nsIContentSecurityPolicy; +interface nsIMutableArray; +interface nsILayoutHistoryState; +interface nsIContentViewer; +interface nsIURI; +interface nsIInputStream; +interface nsIDocShellTreeItem; +interface nsIStructuredCloneContainer; +interface nsIBFCacheEntry; +interface nsIPrincipal; +interface nsISHistory; +interface nsIReferrerInfo; + +%{C++ +#include "nsRect.h" +class nsDocShellEditorData; + +namespace mozilla { +namespace dom { + +class SHEntrySharedParentState; + +} +} +class nsSHEntryShared; +class nsDocShellLoadState; +struct EntriesAndBrowsingContextData; +%} +[ref] native nsIntRect(nsIntRect); +[ptr] native nsDocShellEditorDataPtr(nsDocShellEditorData); +[ptr] native nsDocShellLoadStatePtr(nsDocShellLoadState); +[ptr] native SHEntrySharedParentStatePtr(mozilla::dom::SHEntrySharedParentState); +webidl BrowsingContext; + +[builtinclass, scriptable, uuid(0dad26b8-a259-42c7-93f1-2fa7fc076e45)] +interface nsISHEntry : nsISupports +{ + /** + * The URI of the current entry. + */ + [infallible] attribute nsIURI URI; + + /** + * The original URI of the current entry. If an entry is the result of a + * redirect this attribute holds the original URI. + */ + [infallible] attribute nsIURI originalURI; + + /** + * URL as stored from nsILoadInfo.resultPrincipalURI. See nsILoadInfo + * for more details. + */ + [infallible] attribute nsIURI resultPrincipalURI; + + /** + * If non-null, the URI as it was before query stripping was performed. + */ + [infallible] attribute nsIURI unstrippedURI; + + /** + * This flag remembers whether channel has LOAD_REPLACE set. + */ + [infallible] attribute boolean loadReplace; + + /** + * The title of the current entry. + */ + // XXX: make it [infallible] when AString supports that (bug 1491187). + attribute AString title; + + /** + * The name of the browsing context. + */ + attribute AString name; + + /** + * Was the entry created as a result of a subframe navigation? + * - Will be 'false' when a frameset page is visited for the first time. + * - Will be 'true' for all history entries created as a result of a + * subframe navigation. + */ + [infallible] attribute boolean isSubFrame; + + /** + * Whether the user interacted with the page while this entry was active. + * This includes interactions with subframe documents associated with + * child entries that are rooted at this entry. + * This field will only be set on top-level entries. + */ + [infallible] attribute boolean hasUserInteraction; + + /** + * Whether the load that created this entry was triggered by user activation. + * (e.g.: The user clicked a link) + * Remembering this flag enables replaying the sec-fetch-* headers. + */ + [infallible] attribute boolean hasUserActivation; + + /** Referrer Info*/ + [infallible] attribute nsIReferrerInfo referrerInfo; + + /** Content viewer, for fast restoration of presentation */ + [infallible] attribute nsIContentViewer contentViewer; + + [infallible] readonly attribute boolean isInBFCache; + + /** Whether the content viewer is marked "sticky" */ + [infallible] attribute boolean sticky; + + /** Saved state of the global window object */ + [infallible] attribute nsISupports windowState; + + /** Saved refresh URI list for the content viewer */ + [infallible] attribute nsIMutableArray refreshURIList; + + /** Post Data for the document */ + [infallible] attribute nsIInputStream postData; + [infallible] readonly attribute boolean hasPostData; + + /** LayoutHistoryState for scroll position and form values */ + [infallible] attribute nsILayoutHistoryState layoutHistoryState; + + /** parent of this entry */ + [infallible] attribute nsISHEntry parent; + + /** + * The loadType for this entry. This is typically loadHistory except + * when reload is pressed, it has the appropriate reload flag + */ + [infallible] attribute unsigned long loadType; + + /** + * An ID to help identify this entry from others during + * subframe navigation + */ + [infallible] attribute unsigned long ID; + + /** The cache key for the entry */ + [infallible] attribute unsigned long cacheKey; + + /** Should the layoutHistoryState be saved? */ + [infallible] attribute boolean saveLayoutStateFlag; + + /** + * attribute to indicate the content-type of the document that this + * is a session history entry for + */ + // XXX: make it [infallible] when ACString supports that (bug 1491187). + attribute ACString contentType; + + /** + * If we created this SHEntry via history.pushState or modified it via + * history.replaceState, and if we changed the SHEntry's URI via the + * push/replaceState call, and if the SHEntry's new URI differs from its + * old URI by more than just the hash, then we set this field to true. + * + * Additionally, if this SHEntry was created by calling pushState from a + * SHEntry whose URI was modified, this SHEntry's URIWasModified field is + * true. + */ + [infallible] attribute boolean URIWasModified; + + /** + * Get the principal, if any, that was associated with the channel + * that the document that was loaded to create this history entry + * came from. + */ + [infallible] attribute nsIPrincipal triggeringPrincipal; + + /** + * Get the principal, if any, that is used when the inherit flag + * is set. + */ + [infallible] attribute nsIPrincipal principalToInherit; + + /** + * Get the storage principal, if any, that is used when the inherit flag is + * set. + */ + [infallible] attribute nsIPrincipal partitionedPrincipalToInherit; + + /** + * Get the csp, if any, that was used for this document load. That + * is not the CSP that was applied to subresource loads within the + * document, but the CSP that was applied to this document load. + */ + [infallible] attribute nsIContentSecurityPolicy csp; + + /** + * Get/set data associated with this history state via a pushState() call, + * serialized using structured clone. + **/ + [infallible] attribute nsIStructuredCloneContainer stateData; + + /** + * The history ID of the docshell. + */ + // Would be [infallible], but we don't support that property for nsIDPtr. + attribute nsIDRef docshellID; + + /** + * True if this SHEntry corresponds to a document created by a srcdoc + * iframe. Set when a value is assigned to srcdocData. + */ + [infallible] readonly attribute boolean isSrcdocEntry; + + /** + * Contents of the srcdoc attribute in a srcdoc iframe to be loaded instead + * of the URI. Similar to a Data URI, this information is needed to + * recreate the document at a later stage. + * Setting this sets isSrcdocEntry to true + */ + // XXX: make it [infallible] when AString supports that (bug 1491187). + attribute AString srcdocData; + + /** + * When isSrcdocEntry is true, this contains the baseURI of the srcdoc + * document for use in situations where it cannot otherwise be determined, + * for example with view-source. + */ + [infallible] attribute nsIURI baseURI; + + /** + * Sets/gets the current scroll restoration state, + * if true == "manual", false == "auto". + */ + [infallible] attribute boolean scrollRestorationIsManual; + + /** + * Flag to indicate that the history entry was originally loaded in the + * current process. This flag does not survive a browser process switch. + */ + [infallible] readonly attribute boolean loadedInThisProcess; + + /** + * The session history it belongs to. This is set only on the root entries. + */ + [noscript, infallible] attribute nsISHistory shistory; + + /** + * A number that is assigned by the sHistory when the entry is activated + */ + [noscript, infallible] attribute unsigned long lastTouched; + + /** + * The current number of nsISHEntries which are immediate children of this + * SHEntry. + */ + [infallible] readonly attribute long childCount; + + /** + * When an entry is serving is within nsISHistory's array of entries, this + * property specifies if it should persist. If not it will be replaced by + * new additions to the list. + */ + [infallible] attribute boolean persist; + + /** + * Set/Get the visual viewport scroll position if session history is + * changed through anchor navigation or pushState. + */ + void setScrollPosition(in long x, in long y); + void getScrollPosition(out long x, out long y); + + /** + * Saved position and dimensions of the content viewer; we must adjust the + * root view's widget accordingly if this has changed when the presentation + * is restored. + */ + [noscript, notxpcom] void getViewerBounds(in nsIntRect bounds); + [noscript, notxpcom] void setViewerBounds([const] in nsIntRect bounds); + + /** + * Saved child docshells corresponding to contentViewer. The child shells + * are restored as children of the parent docshell, in this order, when the + * parent docshell restores a saved presentation. + */ + + /** Append a child shell to the end of our list. */ + [noscript, notxpcom] void addChildShell(in nsIDocShellTreeItem shell); + + /** + * Get the child shell at |index|; returns null if |index| is out of bounds. + */ + [noscript] nsIDocShellTreeItem childShellAt(in long index); + + /** + * Clear the child shell list. + */ + [noscript, notxpcom] void clearChildShells(); + + /** + * Ensure that the cached presentation members are self-consistent. + * If either |contentViewer| or |windowState| are null, then all of the + * following members are cleared/reset: + * contentViewer, sticky, windowState, viewerBounds, childShells, + * refreshURIList. + */ + [noscript, notxpcom] void syncPresentationState(); + + /** + * Initialises `layoutHistoryState` if it doesn't already exist + * and returns a reference to it. + */ + nsILayoutHistoryState initLayoutHistoryState(); + + /** Additional ways to create an entry */ + [noscript] void create(in nsIURI URI, in AString title, + in nsIInputStream inputStream, + in unsigned long cacheKey, + in ACString contentType, + in nsIPrincipal triggeringPrincipal, + in nsIPrincipal principalToInherit, + in nsIPrincipal partitionedPrincipalToInherit, + in nsIContentSecurityPolicy aCsp, + in nsIDRef docshellID, + in boolean dynamicCreation, + in nsIURI originalURI, + in nsIURI resultPrincipalURI, + in nsIURI unstrippedURI, + in bool loadReplace, + in nsIReferrerInfo referrerInfo, + in AString srcdoc, + in bool srcdocEntry, + in nsIURI baseURI, + in bool saveLayoutState, + in bool expired, + in bool userActivation); + + nsISHEntry clone(); + + /** + * Gets the owning pointer to the editor data assosicated with + * this shistory entry. This forgets its pointer, so free it when + * you're done. + */ + [noscript, notxpcom] nsDocShellEditorDataPtr forgetEditorData(); + + /** + * Sets the owning pointer to the editor data assosicated with + * this shistory entry. Unless forgetEditorData() is called, this + * shentry will destroy the editor data when it's destroyed. + */ + [noscript, notxpcom] void setEditorData(in nsDocShellEditorDataPtr aData); + + /** Returns true if this shistory entry is storing a detached editor. */ + [noscript, notxpcom] boolean hasDetachedEditor(); + + /** + * Returns true if the related docshell was added because of + * dynamic addition of an iframe/frame. + */ + [noscript, notxpcom] boolean isDynamicallyAdded(); + + /** + * Returns true if any of the child entries returns true + * when isDynamicallyAdded is called on it. + */ + boolean hasDynamicallyAddedChild(); + + /** + * Does this SHEntry point to the given BFCache entry? If so, evicting + * the BFCache entry will evict the SHEntry, since the two entries + * correspond to the same document. + */ + [noscript, notxpcom] + boolean hasBFCacheEntry(in SHEntrySharedParentStatePtr aEntry); + + /** + * Adopt aEntry's BFCacheEntry, so now both this and aEntry point to + * aEntry's BFCacheEntry. + */ + void adoptBFCacheEntry(in nsISHEntry aEntry); + + /** + * Create a new BFCache entry and drop our reference to our old one. This + * call unlinks this SHEntry from any other SHEntries for its document. + */ + void abandonBFCacheEntry(); + + /** + * Does this SHEntry correspond to the same document as aEntry? This is + * true iff the two SHEntries have the same BFCacheEntry. So in particular, + * sharesDocumentWith(aEntry) is guaranteed to return true if it's + * preceded by a call to adoptBFCacheEntry(aEntry). + */ + boolean sharesDocumentWith(in nsISHEntry aEntry); + + /** + * Sets an SHEntry to reflect that it is a history type load. This is the + * equivalent to doing + * + * shEntry.loadType = 4; + * + * in js, but is easier to maintain and less opaque. + */ + void setLoadTypeAsHistory(); + + /** + * Add a new child SHEntry. If offset is -1 adds to the end of the list. + */ + void AddChild(in nsISHEntry aChild, in long aOffset, + [optional,default(false)] in bool aUseRemoteSubframes); + + /** + * Remove a child SHEntry. + */ + [noscript] void RemoveChild(in nsISHEntry aChild); + + /** + * Get child at an index. + */ + nsISHEntry GetChildAt(in long aIndex); + + /** + * If this entry has no dynamically added child, get the child SHEntry + * at the given offset. The loadtype of the returned entry is set + * to its parent's loadtype. + */ + [notxpcom] void GetChildSHEntryIfHasNoDynamicallyAddedChild(in long aChildOffset, + out nsISHEntry aChild); + + /** + * Replaces a child which is for the same docshell as aNewChild + * with aNewChild. + * @throw if nothing was replaced. + */ + [noscript] void ReplaceChild(in nsISHEntry aNewChild); + + /** + * Remove all children of this entry and call abandonBFCacheEntry. + */ + [notxpcom] void ClearEntry(); + + /** + * Create nsDocShellLoadState and fill it with information. + * Don't set nsSHEntry here to avoid serializing it. + */ + [noscript] nsDocShellLoadStatePtr CreateLoadInfo(); + + [infallible] readonly attribute unsigned long long bfcacheID; + + /** + * Sync up the docshell and session history trees for subframe navigation. + * + * @param aEntry new entry + * @param aTopBC top BC corresponding to the root ancestor + of the docshell that called this method + * @param aIgnoreBC current BC + */ + [notxpcom] void SyncTreesForSubframeNavigation(in nsISHEntry aEntry, + in BrowsingContext aTopBC, + in BrowsingContext aIgnoreBC); + + /** + * If browser.history.collectWireframes is true, this will get populated + * with a Wireframe upon document navigation / pushState. This will only + * be set for nsISHEntry's accessed in the parent process with + * sessionHistoryInParent enabled. See Document.webidl for more details on + * what a Wireframe is. + */ + [implicit_jscontext] attribute jsval wireframe; +}; diff --git a/docshell/shistory/nsISHistory.idl b/docshell/shistory/nsISHistory.idl new file mode 100644 index 0000000000..8ea23693d9 --- /dev/null +++ b/docshell/shistory/nsISHistory.idl @@ -0,0 +1,291 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +interface nsIBFCacheEntry; +interface nsISHEntry; +interface nsISHistoryListener; +interface nsIURI; +webidl BrowsingContext; + +%{C++ +#include "nsTArrayForwardDeclare.h" +#include "mozilla/Maybe.h" +struct EntriesAndBrowsingContextData; +namespace mozilla { +namespace dom { +class SHEntrySharedParentState; +} // namespace dom +} // namespace mozilla +%} + +[ref] native nsDocshellIDArray(nsTArray<nsID>); +native MaybeInt32(mozilla::Maybe<int32_t>); +[ptr] native SHEntrySharedParentStatePtr(mozilla::dom::SHEntrySharedParentState); +/** + * An interface to the primary properties of the Session History + * component. In an embedded browser environment, the nsIWebBrowser + * object creates an instance of session history for each open window. + * A handle to the session history object can be obtained from + * nsIWebNavigation. In a non-embedded situation, the owner of the + * session history component must create a instance of it and set + * it in the nsIWebNavigation object. + * This interface is accessible from javascript. + */ + +[builtinclass, scriptable, uuid(7b807041-e60a-4384-935f-af3061d8b815)] +interface nsISHistory: nsISupports +{ + /** + * A readonly property of the interface that returns + * the number of toplevel documents currently available + * in session history. + */ + [infallible] readonly attribute long count; + + /** + * The index of the current document in session history. Not infallible + * because setting can fail if the assigned value is out of range. + */ + attribute long index; + + /** + * A readonly property of the interface that returns + * the index of the last document that started to load and + * didn't finished yet. When document finishes the loading + * value -1 is returned. + */ + [infallible] readonly attribute long requestedIndex; + + /** + * Artifically set the |requestedIndex| for this nsISHEntry to the given + * index. This is used when resuming a cross-process load from a different + * process. + */ + [noscript, notxpcom] + void internalSetRequestedIndex(in long aRequestedIndex); + + /** + * Get the history entry at a given index. Returns non-null on success. + * + * @param index The index value whose entry is requested. + * The oldest entry is located at index == 0. + * @return The found entry; never null. + */ + nsISHEntry getEntryAtIndex(in long aIndex); + + /** + * Called to purge older documents from history. + * Documents can be removed from session history for various + * reasons. For example to control memory usage of the browser, to + * prevent users from loading documents from history, to erase evidence of + * prior page loads etc... + * + * @param numEntries The number of toplevel documents to be + * purged from history. During purge operation, + * the latest documents are maintained and older + * 'numEntries' documents are removed from history. + * @throws <code>NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA</code> + * Purge was vetod. + * @throws <code>NS_ERROR_FAILURE</code> numEntries is + * invalid or out of bounds with the size of history. + */ + void purgeHistory(in long aNumEntries); + + /** + * Called to register a listener for the session history component. + * Listeners are notified when pages are loaded or purged from history. + * + * @param aListener Listener object to be notified for all + * page loads that initiate in session history. + * + * @note A listener object must implement + * nsISHistoryListener and nsSupportsWeakReference + * + * @see nsISHistoryListener + * @see nsSupportsWeakReference + */ + void addSHistoryListener(in nsISHistoryListener aListener); + + /** + * Called to remove a listener for the session history component. + * Listeners are notified when pages are loaded from history. + * + * @param aListener Listener object to be removed from + * session history. + * + * @note A listener object must implement + * nsISHistoryListener and nsSupportsWeakReference + * @see nsISHistoryListener + * @see nsSupportsWeakReference + */ + void removeSHistoryListener(in nsISHistoryListener aListener); + + void reloadCurrentEntry(); + + /** + * Load the entry at the particular index. + */ + [noscript] + void gotoIndex(in long aIndex, in boolean aUserActivation); + + /** + * If an element exists at the particular index and + * whether it has user interaction. + */ + [noscript,notxpcom] + boolean hasUserInteractionAtIndex(in long aIndex); + + /** + * Called to obtain the index to a given history entry. + * + * @param aEntry The entry to obtain the index of. + * + * @return <code>NS_OK</code> index for the history entry + * is obtained successfully. + * <code>NS_ERROR_FAILURE</code> Error in obtaining + * index for the given history entry. + */ + [noscript, notxpcom] + long getIndexOfEntry(in nsISHEntry aEntry); + + /** + * Add a new Entry to the History List. + * + * @param aEntry The entry to add. + * @param aPersist If true this specifies that the entry should + * persist in the list. If false, this means that + * when new entries are added this element will not + * appear in the session history list. + */ + void addEntry(in nsISHEntry aEntry, in boolean aPersist); + + /** + * Update the index maintained by sessionHistory + */ + void updateIndex(); + + /** + * Replace the nsISHEntry at a particular index + * + * @param aIndex The index at which the entry should be replaced. + * @param aReplaceEntry The replacement entry for the index. + */ + void replaceEntry(in long aIndex, in nsISHEntry aReplaceEntry); + + /** + * Notifies all registered session history listeners about an impending + * reload. + * + * @return Whether the operation can proceed. + */ + boolean notifyOnHistoryReload(); + + /** + * Evict content viewers which don't lie in the "safe" range around aIndex. + * In practice, this should leave us with no more than gHistoryMaxViewers + * viewers associated with this SHistory object. + * + * Also make sure that the total number of content viewers in all windows is + * not greater than our global max; if it is, evict viewers as appropriate. + * + * @param aIndex The index around which the "safe" range is + * centered. In general, if you just navigated the + * history, aIndex should be the index history was + * navigated to. + */ + void evictOutOfRangeContentViewers(in long aIndex); + + /** + * Evict the content viewer associated with a bfcache entry that has timed + * out. + */ + [noscript, notxpcom] + void evictExpiredContentViewerForEntry(in SHEntrySharedParentStatePtr aEntry); + + /** + * Evict all the content viewers in this session history + */ + void evictAllContentViewers(); + + /** + * Add a BFCache entry to expiration tracker so it gets evicted on + * expiration. + */ + [noscript, notxpcom] + void addToExpirationTracker(in SHEntrySharedParentStatePtr aEntry); + + /** + * Remove a BFCache entry from expiration tracker. + */ + [noscript, notxpcom] + void removeFromExpirationTracker(in SHEntrySharedParentStatePtr aEntry); + + /** + * Remove dynamic entries found at given index. + * + * @param aIndex Index to remove dynamic entries from. It will be + * passed to RemoveEntries as aStartIndex. + * @param aEntry (optional) The entry to start looking in for dynamic + * entries. Only the dynamic descendants of the + * entry will be removed. If not given, all dynamic + * entries at the index will be removed. + */ + [noscript, notxpcom] + void RemoveDynEntries(in long aIndex, in nsISHEntry aEntry); + + /** + * Similar to RemoveDynEntries, but instead of specifying an index, use the + * given BFCacheEntry to find the index and remove dynamic entries from the + * index. + * + * The method takes no effect if the bfcache entry is not or no longer hold + * by the SHistory instance. + * + * @param aEntry The bfcache entry to look up for index to remove + * dynamic entries from. + */ + [noscript, notxpcom] + void RemoveDynEntriesForBFCacheEntry(in nsIBFCacheEntry aEntry); + + /** + * Removes entries from the history if their docshellID is in + * aIDs array. + */ + [noscript, notxpcom] + void RemoveEntries(in nsDocshellIDArray aIDs, in long aStartIndex); + + /** + * Collect docshellIDs from aEntry's children and remove those + * entries from history. + * + * @param aEntry Children docshellID's will be collected from + * this entry and passed to RemoveEntries as aIDs. + */ + [noscript, notxpcom] + void RemoveFrameEntries(in nsISHEntry aEntry); + + [noscript] + void Reload(in unsigned long aReloadFlags); + + [notxpcom] void EnsureCorrectEntryAtCurrIndex(in nsISHEntry aEntry); + + [notxpcom] void EvictContentViewersOrReplaceEntry(in nsISHEntry aNewSHEntry, in bool aReplace); + + nsISHEntry createEntry(); + + [noscript] void AddToRootSessionHistory(in bool aCloneChildren, in nsISHEntry aOSHE, + in BrowsingContext aRootBC, in nsISHEntry aEntry, + in unsigned long aLoadType, + in bool aShouldPersist, + out MaybeInt32 aPreviousEntryIndex, + out MaybeInt32 aLoadedEntryIndex); + + [noscript] void AddChildSHEntryHelper(in nsISHEntry aCloneRef, in nsISHEntry aNewEntry, + in BrowsingContext aRootBC, in bool aCloneChildren); + + [noscript, notxpcom] boolean isEmptyOrHasEntriesForSingleTopLevelPage(); +}; diff --git a/docshell/shistory/nsISHistoryListener.idl b/docshell/shistory/nsISHistoryListener.idl new file mode 100644 index 0000000000..569cb25dca --- /dev/null +++ b/docshell/shistory/nsISHistoryListener.idl @@ -0,0 +1,88 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "nsISupports.idl" + +interface nsIURI; + +/** + * nsISHistoryListener defines the interface one can implement to receive + * notifications about activities in session history and (for reloads) to be + * able to cancel them. + * + * A session history listener will be notified when pages are added, removed + * and loaded from session history. In the case of reloads, it can prevent them + * from happening by returning false from the corresponding callback method. + * + * A session history listener can be registered on a particular nsISHistory + * instance via the nsISHistory::addSHistoryListener() method. + * + * Listener methods should not alter the session history. Things are likely to + * go haywire if they do. + */ +[scriptable, uuid(125c0833-746a-400e-9b89-d2d18545c08a)] +interface nsISHistoryListener : nsISupports +{ + /** + * Called when a new document is added to session history. New documents are + * added to session history by docshell when new pages are loaded in a frame + * or content area, for example via nsIWebNavigation::loadURI() + * + * @param aNewURI The URI of the document to be added to session history. + * @param aOldIndex The index of the current history item before the + * operation. + */ + void OnHistoryNewEntry(in nsIURI aNewURI, in long aOldIndex); + + /** + * Called before the current document is reloaded, for example due to a + * nsIWebNavigation::reload() call. + */ + boolean OnHistoryReload(); + + /** + * Called before navigating to a session history entry by index, for example, + * when nsIWebNavigation::gotoIndex() is called. + */ + void OnHistoryGotoIndex(); + + /** + * Called before entries are removed from the start of session history. + * Entries can be removed from session history for various reasons, for + * example to control the memory usage of the browser, to prevent users from + * loading documents from history, to erase evidence of prior page loads, etc. + * + * To purge documents from session history call nsISHistory::PurgeHistory(). + * + * @param aNumEntries The number of entries being removed. + */ + void OnHistoryPurge(in long aNumEntries); + + /** + * Called before entries are removed from the end of session history. This + * occurs when navigating to a new page while on a previous session entry. + * + * @param aNumEntries The number of entries being removed. + */ + void OnHistoryTruncate(in long aNumEntries); + + /** + * Called before an entry is replaced in the session history. Entries are + * replaced when navigating away from non-persistent history entries (such as + * about pages) and when history.replaceState is called. + */ + void OnHistoryReplaceEntry(); + + + /** + * Called whenever a content viewer is evicted. A content viewer is evicted + * whenever a bfcache entry has timed out or the number of total content + * viewers has exceeded the global max. This is used for testing only. + * + * @param aNumEvicted - number of content viewers evicted + */ + void OnContentViewerEvicted(in unsigned long aNumEvicted); +}; diff --git a/docshell/shistory/nsSHEntry.cpp b/docshell/shistory/nsSHEntry.cpp new file mode 100644 index 0000000000..0b250c33ff --- /dev/null +++ b/docshell/shistory/nsSHEntry.cpp @@ -0,0 +1,1131 @@ +/* -*- 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/. */ + +#include "nsSHEntry.h" + +#include <algorithm> + +#include "nsDocShell.h" +#include "nsDocShellEditorData.h" +#include "nsDocShellLoadState.h" +#include "nsDocShellLoadTypes.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIContentViewer.h" +#include "nsIDocShellTreeItem.h" +#include "nsIInputStream.h" +#include "nsILayoutHistoryState.h" +#include "nsIMutableArray.h" +#include "nsIStructuredCloneContainer.h" +#include "nsIURI.h" +#include "nsSHEntryShared.h" +#include "nsSHistory.h" + +#include "mozilla/Logging.h" +#include "nsIReferrerInfo.h" + +extern mozilla::LazyLogModule gPageCacheLog; + +static uint32_t gEntryID = 0; + +nsSHEntry::nsSHEntry() + : mShared(new nsSHEntryShared()), + mLoadType(0), + mID(++gEntryID), // SessionStore has special handling for 0 values. + mScrollPositionX(0), + mScrollPositionY(0), + mLoadReplace(false), + mURIWasModified(false), + mIsSrcdocEntry(false), + mScrollRestorationIsManual(false), + mLoadedInThisProcess(false), + mPersist(true), + mHasUserInteraction(false), + mHasUserActivation(false) {} + +nsSHEntry::nsSHEntry(const nsSHEntry& aOther) + : mShared(aOther.mShared), + mURI(aOther.mURI), + mOriginalURI(aOther.mOriginalURI), + mResultPrincipalURI(aOther.mResultPrincipalURI), + mUnstrippedURI(aOther.mUnstrippedURI), + mReferrerInfo(aOther.mReferrerInfo), + mTitle(aOther.mTitle), + mPostData(aOther.mPostData), + mLoadType(0), // XXX why not copy? + mID(aOther.mID), + mScrollPositionX(0), // XXX why not copy? + mScrollPositionY(0), // XXX why not copy? + mParent(aOther.mParent), + mStateData(aOther.mStateData), + mSrcdocData(aOther.mSrcdocData), + mBaseURI(aOther.mBaseURI), + mLoadReplace(aOther.mLoadReplace), + mURIWasModified(aOther.mURIWasModified), + mIsSrcdocEntry(aOther.mIsSrcdocEntry), + mScrollRestorationIsManual(false), + mLoadedInThisProcess(aOther.mLoadedInThisProcess), + mPersist(aOther.mPersist), + mHasUserInteraction(false), + mHasUserActivation(aOther.mHasUserActivation) {} + +nsSHEntry::~nsSHEntry() { + // Null out the mParent pointers on all our kids. + for (nsISHEntry* entry : mChildren) { + if (entry) { + entry->SetParent(nullptr); + } + } +} + +NS_IMPL_ISUPPORTS(nsSHEntry, nsISHEntry, nsISupportsWeakReference) + +NS_IMETHODIMP +nsSHEntry::SetScrollPosition(int32_t aX, int32_t aY) { + mScrollPositionX = aX; + mScrollPositionY = aY; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetScrollPosition(int32_t* aX, int32_t* aY) { + *aX = mScrollPositionX; + *aY = mScrollPositionY; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetURIWasModified(bool* aOut) { + *aOut = mURIWasModified; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetURIWasModified(bool aIn) { + mURIWasModified = aIn; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetURI(nsIURI** aURI) { + *aURI = mURI; + NS_IF_ADDREF(*aURI); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetURI(nsIURI* aURI) { + mURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetOriginalURI(nsIURI** aOriginalURI) { + *aOriginalURI = mOriginalURI; + NS_IF_ADDREF(*aOriginalURI); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetOriginalURI(nsIURI* aOriginalURI) { + mOriginalURI = aOriginalURI; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetResultPrincipalURI(nsIURI** aResultPrincipalURI) { + *aResultPrincipalURI = mResultPrincipalURI; + NS_IF_ADDREF(*aResultPrincipalURI); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetResultPrincipalURI(nsIURI* aResultPrincipalURI) { + mResultPrincipalURI = aResultPrincipalURI; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetUnstrippedURI(nsIURI** aUnstrippedURI) { + *aUnstrippedURI = mUnstrippedURI; + NS_IF_ADDREF(*aUnstrippedURI); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetUnstrippedURI(nsIURI* aUnstrippedURI) { + mUnstrippedURI = aUnstrippedURI; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetLoadReplace(bool* aLoadReplace) { + *aLoadReplace = mLoadReplace; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetLoadReplace(bool aLoadReplace) { + mLoadReplace = aLoadReplace; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) { + *aReferrerInfo = mReferrerInfo; + NS_IF_ADDREF(*aReferrerInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + mReferrerInfo = aReferrerInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetSticky(bool aSticky) { + mShared->mSticky = aSticky; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetSticky(bool* aSticky) { + *aSticky = mShared->mSticky; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetTitle(nsAString& aTitle) { + // Check for empty title... + if (mTitle.IsEmpty() && mURI) { + // Default title is the URL. + nsAutoCString spec; + if (NS_SUCCEEDED(mURI->GetSpec(spec))) { + AppendUTF8toUTF16(spec, mTitle); + } + } + + aTitle = mTitle; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetTitle(const nsAString& aTitle) { + mTitle = aTitle; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetName(nsAString& aName) { + aName = mName; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetName(const nsAString& aName) { + mName = aName; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetPostData(nsIInputStream** aResult) { + *aResult = mPostData; + NS_IF_ADDREF(*aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetPostData(nsIInputStream* aPostData) { + mPostData = aPostData; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetHasPostData(bool* aResult) { + *aResult = !!mPostData; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetLayoutHistoryState(nsILayoutHistoryState** aResult) { + *aResult = mShared->mLayoutHistoryState; + NS_IF_ADDREF(*aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetLayoutHistoryState(nsILayoutHistoryState* aState) { + mShared->mLayoutHistoryState = aState; + if (mShared->mLayoutHistoryState) { + mShared->mLayoutHistoryState->SetScrollPositionOnly( + !mShared->mSaveLayoutState); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::InitLayoutHistoryState(nsILayoutHistoryState** aState) { + if (!mShared->mLayoutHistoryState) { + nsCOMPtr<nsILayoutHistoryState> historyState; + historyState = NS_NewLayoutHistoryState(); + SetLayoutHistoryState(historyState); + } + + nsCOMPtr<nsILayoutHistoryState> state = GetLayoutHistoryState(); + state.forget(aState); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetLoadType(uint32_t* aResult) { + *aResult = mLoadType; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetLoadType(uint32_t aLoadType) { + mLoadType = aLoadType; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetID(uint32_t* aResult) { + *aResult = mID; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetID(uint32_t aID) { + mID = aID; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetIsSubFrame(bool* aFlag) { + *aFlag = mShared->mIsFrameNavigation; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetIsSubFrame(bool aFlag) { + mShared->mIsFrameNavigation = aFlag; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetHasUserInteraction(bool* aFlag) { + // The back button and menulist deal with root/top-level + // session history entries, thus we annotate only the root entry. + if (!mParent) { + *aFlag = mHasUserInteraction; + } else { + nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(this); + root->GetHasUserInteraction(aFlag); + } + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetHasUserInteraction(bool aFlag) { + // The back button and menulist deal with root/top-level + // session history entries, thus we annotate only the root entry. + if (!mParent) { + mHasUserInteraction = aFlag; + } else { + nsCOMPtr<nsISHEntry> root = nsSHistory::GetRootSHEntry(this); + root->SetHasUserInteraction(aFlag); + } + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetHasUserActivation(bool* aFlag) { + *aFlag = mHasUserActivation; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetHasUserActivation(bool aFlag) { + mHasUserActivation = aFlag; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetCacheKey(uint32_t* aResult) { + *aResult = mShared->mCacheKey; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetCacheKey(uint32_t aCacheKey) { + mShared->mCacheKey = aCacheKey; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetContentType(nsACString& aContentType) { + aContentType = mShared->mContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetContentType(const nsACString& aContentType) { + mShared->mContentType = aContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::Create( + nsIURI* aURI, const nsAString& aTitle, nsIInputStream* aInputStream, + uint32_t aCacheKey, const nsACString& aContentType, + nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, const nsID& aDocShellID, + bool aDynamicCreation, nsIURI* aOriginalURI, nsIURI* aResultPrincipalURI, + nsIURI* aUnstrippedURI, bool aLoadReplace, nsIReferrerInfo* aReferrerInfo, + const nsAString& aSrcdocData, bool aSrcdocEntry, nsIURI* aBaseURI, + bool aSaveLayoutState, bool aExpired, bool aUserActivation) { + MOZ_ASSERT( + aTriggeringPrincipal, + "need a valid triggeringPrincipal to create a session history entry"); + + mURI = aURI; + mTitle = aTitle; + mPostData = aInputStream; + + // Set the LoadType by default to loadHistory during creation + mLoadType = LOAD_HISTORY; + + mShared->mCacheKey = aCacheKey; + mShared->mContentType = aContentType; + mShared->mTriggeringPrincipal = aTriggeringPrincipal; + mShared->mPrincipalToInherit = aPrincipalToInherit; + mShared->mPartitionedPrincipalToInherit = aPartitionedPrincipalToInherit; + mShared->mCsp = aCsp; + mShared->mDocShellID = aDocShellID; + mShared->mDynamicallyCreated = aDynamicCreation; + + // By default all entries are set false for subframe flag. + // nsDocShell::CloneAndReplace() which creates entries for + // all subframe navigations, sets the flag to true. + mShared->mIsFrameNavigation = false; + + mHasUserInteraction = false; + + mShared->mExpired = aExpired; + + mIsSrcdocEntry = aSrcdocEntry; + mSrcdocData = aSrcdocData; + + mBaseURI = aBaseURI; + + mLoadedInThisProcess = true; + + mOriginalURI = aOriginalURI; + mResultPrincipalURI = aResultPrincipalURI; + mUnstrippedURI = aUnstrippedURI; + mLoadReplace = aLoadReplace; + mReferrerInfo = aReferrerInfo; + + mHasUserActivation = aUserActivation; + + mShared->mLayoutHistoryState = nullptr; + + mShared->mSaveLayoutState = aSaveLayoutState; + + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetParent(nsISHEntry** aResult) { + nsCOMPtr<nsISHEntry> parent = do_QueryReferent(mParent); + parent.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetParent(nsISHEntry* aParent) { + mParent = do_GetWeakReference(aParent); + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHEntry::SetViewerBounds(const nsIntRect& aBounds) { + mShared->mViewerBounds = aBounds; +} + +NS_IMETHODIMP_(void) +nsSHEntry::GetViewerBounds(nsIntRect& aBounds) { + aBounds = mShared->mViewerBounds; +} + +NS_IMETHODIMP +nsSHEntry::GetTriggeringPrincipal(nsIPrincipal** aTriggeringPrincipal) { + NS_IF_ADDREF(*aTriggeringPrincipal = mShared->mTriggeringPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetTriggeringPrincipal(nsIPrincipal* aTriggeringPrincipal) { + mShared->mTriggeringPrincipal = aTriggeringPrincipal; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetPrincipalToInherit(nsIPrincipal** aPrincipalToInherit) { + NS_IF_ADDREF(*aPrincipalToInherit = mShared->mPrincipalToInherit); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetPrincipalToInherit(nsIPrincipal* aPrincipalToInherit) { + mShared->mPrincipalToInherit = aPrincipalToInherit; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetPartitionedPrincipalToInherit( + nsIPrincipal** aPartitionedPrincipalToInherit) { + NS_IF_ADDREF(*aPartitionedPrincipalToInherit = + mShared->mPartitionedPrincipalToInherit); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetPartitionedPrincipalToInherit( + nsIPrincipal* aPartitionedPrincipalToInherit) { + mShared->mPartitionedPrincipalToInherit = aPartitionedPrincipalToInherit; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetCsp(nsIContentSecurityPolicy** aCsp) { + NS_IF_ADDREF(*aCsp = mShared->mCsp); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetCsp(nsIContentSecurityPolicy* aCsp) { + mShared->mCsp = aCsp; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::AdoptBFCacheEntry(nsISHEntry* aEntry) { + nsSHEntryShared* shared = static_cast<nsSHEntry*>(aEntry)->mShared; + NS_ENSURE_STATE(shared); + + mShared = shared; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SharesDocumentWith(nsISHEntry* aEntry, bool* aOut) { + NS_ENSURE_ARG_POINTER(aOut); + + *aOut = mShared == static_cast<nsSHEntry*>(aEntry)->mShared; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetIsSrcdocEntry(bool* aIsSrcdocEntry) { + *aIsSrcdocEntry = mIsSrcdocEntry; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetSrcdocData(nsAString& aSrcdocData) { + aSrcdocData = mSrcdocData; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetSrcdocData(const nsAString& aSrcdocData) { + mSrcdocData = aSrcdocData; + mIsSrcdocEntry = true; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetBaseURI(nsIURI** aBaseURI) { + *aBaseURI = mBaseURI; + NS_IF_ADDREF(*aBaseURI); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetBaseURI(nsIURI* aBaseURI) { + mBaseURI = aBaseURI; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetScrollRestorationIsManual(bool* aIsManual) { + *aIsManual = mScrollRestorationIsManual; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetScrollRestorationIsManual(bool aIsManual) { + mScrollRestorationIsManual = aIsManual; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetLoadedInThisProcess(bool* aLoadedInThisProcess) { + *aLoadedInThisProcess = mLoadedInThisProcess; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetChildCount(int32_t* aCount) { + *aCount = mChildren.Count(); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::AddChild(nsISHEntry* aChild, int32_t aOffset, + bool aUseRemoteSubframes) { + if (aChild) { + NS_ENSURE_SUCCESS(aChild->SetParent(this), NS_ERROR_FAILURE); + } + + if (aOffset < 0) { + mChildren.AppendObject(aChild); + return NS_OK; + } + + // + // Bug 52670: Ensure children are added in order. + // + // Later frames in the child list may load faster and get appended + // before earlier frames, causing session history to be scrambled. + // By growing the list here, they are added to the right position. + // + // Assert that aOffset will not be so high as to grow us a lot. + // + NS_ASSERTION(aOffset < (mChildren.Count() + 1023), "Large frames array!\n"); + + bool newChildIsDyn = aChild ? aChild->IsDynamicallyAdded() : false; + + // If the new child is dynamically added, try to add it to aOffset, but if + // there are non-dynamically added children, the child must be after those. + if (newChildIsDyn) { + int32_t lastNonDyn = aOffset - 1; + for (int32_t i = aOffset; i < mChildren.Count(); ++i) { + nsISHEntry* entry = mChildren[i]; + if (entry) { + if (entry->IsDynamicallyAdded()) { + break; + } else { + lastNonDyn = i; + } + } + } + // InsertObjectAt allows only appending one object. + // If aOffset is larger than Count(), we must first manually + // set the capacity. + if (aOffset > mChildren.Count()) { + mChildren.SetCount(aOffset); + } + if (!mChildren.InsertObjectAt(aChild, lastNonDyn + 1)) { + NS_WARNING("Adding a child failed!"); + aChild->SetParent(nullptr); + return NS_ERROR_FAILURE; + } + } else { + // If the new child isn't dynamically added, it should be set to aOffset. + // If there are dynamically added children before that, those must be + // moved to be after aOffset. + if (mChildren.Count() > 0) { + int32_t start = std::min(mChildren.Count() - 1, aOffset); + int32_t dynEntryIndex = -1; + nsISHEntry* dynEntry = nullptr; + for (int32_t i = start; i >= 0; --i) { + nsISHEntry* entry = mChildren[i]; + if (entry) { + if (entry->IsDynamicallyAdded()) { + dynEntryIndex = i; + dynEntry = entry; + } else { + break; + } + } + } + + if (dynEntry) { + nsCOMArray<nsISHEntry> tmp; + tmp.SetCount(aOffset - dynEntryIndex + 1); + mChildren.InsertObjectsAt(tmp, dynEntryIndex); + NS_ASSERTION(mChildren[aOffset + 1] == dynEntry, "Whaat?"); + } + } + + // Make sure there isn't anything at aOffset. + if (aOffset < mChildren.Count()) { + nsISHEntry* oldChild = mChildren[aOffset]; + if (oldChild && oldChild != aChild) { + // Under Fission, this can happen when a network-created iframe starts + // out in-process, moves out-of-process, and then switches back. At that + // point, we'll create a new network-created DocShell at the same index + // where we already have an entry for the original network-created + // DocShell. + // + // This should ideally stop being an issue once the Fission-aware + // session history rewrite is complete. + NS_ASSERTION( + aUseRemoteSubframes, + "Adding a child where we already have a child? This may misbehave"); + oldChild->SetParent(nullptr); + } + } + + mChildren.ReplaceObjectAt(aChild, aOffset); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::RemoveChild(nsISHEntry* aChild) { + NS_ENSURE_TRUE(aChild, NS_ERROR_FAILURE); + bool childRemoved = false; + if (aChild->IsDynamicallyAdded()) { + childRemoved = mChildren.RemoveObject(aChild); + } else { + int32_t index = mChildren.IndexOfObject(aChild); + if (index >= 0) { + // Other alive non-dynamic child docshells still keep mChildOffset, + // so we don't want to change the indices here. + mChildren.ReplaceObjectAt(nullptr, index); + childRemoved = true; + } + } + if (childRemoved) { + aChild->SetParent(nullptr); + + // reduce the child count, i.e. remove empty children at the end + for (int32_t i = mChildren.Count() - 1; i >= 0 && !mChildren[i]; --i) { + if (!mChildren.RemoveObjectAt(i)) { + break; + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetChildAt(int32_t aIndex, nsISHEntry** aResult) { + if (aIndex >= 0 && aIndex < mChildren.Count()) { + *aResult = mChildren[aIndex]; + // yes, mChildren can have holes in it. AddChild's offset parameter makes + // that possible. + NS_IF_ADDREF(*aResult); + } else { + *aResult = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHEntry::GetChildSHEntryIfHasNoDynamicallyAddedChild(int32_t aChildOffset, + nsISHEntry** aChild) { + *aChild = nullptr; + + bool dynamicallyAddedChild = false; + HasDynamicallyAddedChild(&dynamicallyAddedChild); + if (dynamicallyAddedChild) { + return; + } + + // If the user did a shift-reload on this frameset page, + // we don't want to load the subframes from history. + if (IsForceReloadType(mLoadType) || mLoadType == LOAD_REFRESH) { + return; + } + + /* Before looking for the subframe's url, check + * the expiration status of the parent. If the parent + * has expired from cache, then subframes will not be + * loaded from history in certain situations. + * If the user pressed reload and the parent frame has expired + * from cache, we do not want to load the child frame from history. + */ + if (mShared->mExpired && (mLoadType == LOAD_RELOAD_NORMAL)) { + // The parent has expired. Return null. + *aChild = nullptr; + return; + } + // Get the child subframe from session history. + GetChildAt(aChildOffset, aChild); + if (*aChild) { + // Set the parent's Load Type on the child + (*aChild)->SetLoadType(mLoadType); + } +} + +NS_IMETHODIMP +nsSHEntry::ReplaceChild(nsISHEntry* aNewEntry) { + NS_ENSURE_STATE(aNewEntry); + + nsID docshellID; + aNewEntry->GetDocshellID(docshellID); + + for (int32_t i = 0; i < mChildren.Count(); ++i) { + if (mChildren[i]) { + nsID childDocshellID; + nsresult rv = mChildren[i]->GetDocshellID(childDocshellID); + NS_ENSURE_SUCCESS(rv, rv); + if (docshellID == childDocshellID) { + mChildren[i]->SetParent(nullptr); + mChildren.ReplaceObjectAt(aNewEntry, i); + return aNewEntry->SetParent(this); + } + } + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP_(void) nsSHEntry::ClearEntry() { + int32_t childCount = GetChildCount(); + // Remove all children of this entry + for (int32_t i = childCount - 1; i >= 0; i--) { + nsCOMPtr<nsISHEntry> child; + GetChildAt(i, getter_AddRefs(child)); + RemoveChild(child); + } + AbandonBFCacheEntry(); +} + +NS_IMETHODIMP +nsSHEntry::GetStateData(nsIStructuredCloneContainer** aContainer) { + NS_IF_ADDREF(*aContainer = mStateData); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetStateData(nsIStructuredCloneContainer* aContainer) { + mStateData = aContainer; + return NS_OK; +} + +NS_IMETHODIMP_(bool) +nsSHEntry::IsDynamicallyAdded() { return mShared->mDynamicallyCreated; } + +NS_IMETHODIMP +nsSHEntry::HasDynamicallyAddedChild(bool* aAdded) { + *aAdded = false; + for (int32_t i = 0; i < mChildren.Count(); ++i) { + nsISHEntry* entry = mChildren[i]; + if (entry) { + *aAdded = entry->IsDynamicallyAdded(); + if (*aAdded) { + break; + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetDocshellID(nsID& aID) { + aID = mShared->mDocShellID; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetDocshellID(const nsID& aID) { + mShared->mDocShellID = aID; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetLastTouched(uint32_t* aLastTouched) { + *aLastTouched = mShared->mLastTouched; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetLastTouched(uint32_t aLastTouched) { + mShared->mLastTouched = aLastTouched; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetShistory(nsISHistory** aSHistory) { + nsCOMPtr<nsISHistory> shistory(do_QueryReferent(mShared->mSHistory)); + shistory.forget(aSHistory); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetShistory(nsISHistory* aSHistory) { + nsWeakPtr shistory = do_GetWeakReference(aSHistory); + // mSHistory can not be changed once it's set + MOZ_ASSERT(!mShared->mSHistory || (mShared->mSHistory == shistory)); + mShared->mSHistory = shistory; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetLoadTypeAsHistory() { + // Set the LoadType by default to loadHistory during creation + mLoadType = LOAD_HISTORY; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetPersist(bool* aPersist) { + *aPersist = mPersist; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetPersist(bool aPersist) { + mPersist = aPersist; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::CreateLoadInfo(nsDocShellLoadState** aLoadState) { + nsCOMPtr<nsIURI> uri = GetURI(); + RefPtr<nsDocShellLoadState> loadState(new nsDocShellLoadState(uri)); + + nsCOMPtr<nsIURI> originalURI = GetOriginalURI(); + loadState->SetOriginalURI(originalURI); + + mozilla::Maybe<nsCOMPtr<nsIURI>> emplacedResultPrincipalURI; + nsCOMPtr<nsIURI> resultPrincipalURI = GetResultPrincipalURI(); + emplacedResultPrincipalURI.emplace(std::move(resultPrincipalURI)); + loadState->SetMaybeResultPrincipalURI(emplacedResultPrincipalURI); + + nsCOMPtr<nsIURI> unstrippedURI = GetUnstrippedURI(); + loadState->SetUnstrippedURI(unstrippedURI); + + loadState->SetLoadReplace(GetLoadReplace()); + nsCOMPtr<nsIInputStream> postData = GetPostData(); + loadState->SetPostDataStream(postData); + + nsAutoCString contentType; + GetContentType(contentType); + loadState->SetTypeHint(contentType); + + nsCOMPtr<nsIPrincipal> triggeringPrincipal = GetTriggeringPrincipal(); + loadState->SetTriggeringPrincipal(triggeringPrincipal); + nsCOMPtr<nsIPrincipal> principalToInherit = GetPrincipalToInherit(); + loadState->SetPrincipalToInherit(principalToInherit); + nsCOMPtr<nsIPrincipal> partitionedPrincipalToInherit = + GetPartitionedPrincipalToInherit(); + loadState->SetPartitionedPrincipalToInherit(partitionedPrincipalToInherit); + nsCOMPtr<nsIContentSecurityPolicy> csp = GetCsp(); + loadState->SetCsp(csp); + nsCOMPtr<nsIReferrerInfo> referrerInfo = GetReferrerInfo(); + loadState->SetReferrerInfo(referrerInfo); + + // Do not inherit principal from document (security-critical!); + uint32_t flags = nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_NONE; + + // Passing nullptr as aSourceDocShell gives the same behaviour as before + // aSourceDocShell was introduced. According to spec we should be passing + // the source browsing context that was used when the history entry was + // first created. bug 947716 has been created to address this issue. + nsAutoString srcdoc; + nsCOMPtr<nsIURI> baseURI; + if (GetIsSrcdocEntry()) { + GetSrcdocData(srcdoc); + baseURI = GetBaseURI(); + flags |= nsDocShell::InternalLoad::INTERNAL_LOAD_FLAGS_IS_SRCDOC; + } else { + srcdoc = VoidString(); + } + loadState->SetSrcdocData(srcdoc); + loadState->SetBaseURI(baseURI); + loadState->SetInternalLoadFlags(flags); + + loadState->SetFirstParty(true); + + loadState->SetHasValidUserGestureActivation(GetHasUserActivation()); + + loadState->SetSHEntry(this); + + // When we create a load state from the history entry we already know if + // https-first was able to upgrade the request from http to https. There is no + // point in re-retrying to upgrade. + loadState->SetIsExemptFromHTTPSOnlyMode(true); + + loadState.forget(aLoadState); + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHEntry::SyncTreesForSubframeNavigation( + nsISHEntry* aEntry, mozilla::dom::BrowsingContext* aTopBC, + mozilla::dom::BrowsingContext* aIgnoreBC) { + // XXX Keep this in sync with + // SessionHistoryEntry::SyncTreesForSubframeNavigation + // + // We need to sync up the browsing context and session history trees for + // subframe navigation. If the load was in a subframe, we forward up to + // the top browsing context, which will then recursively sync up all browsing + // contexts to their corresponding entries in the new session history tree. If + // we don't do this, then we can cache a content viewer on the wrong cloned + // entry, and subsequently restore it at the wrong time. + nsCOMPtr<nsISHEntry> newRootEntry = nsSHistory::GetRootSHEntry(aEntry); + if (newRootEntry) { + // newRootEntry is now the new root entry. + // Find the old root entry as well. + + // Need a strong ref. on |oldRootEntry| so it isn't destroyed when + // SetChildHistoryEntry() does SwapHistoryEntries() (bug 304639). + nsCOMPtr<nsISHEntry> oldRootEntry = nsSHistory::GetRootSHEntry(this); + + if (oldRootEntry) { + nsSHistory::SwapEntriesData data = {aIgnoreBC, newRootEntry, nullptr}; + nsSHistory::SetChildHistoryEntry(oldRootEntry, aTopBC, 0, &data); + } + } +} + +void nsSHEntry::EvictContentViewer() { + nsCOMPtr<nsIContentViewer> viewer = GetContentViewer(); + if (viewer) { + mShared->NotifyListenersContentViewerEvicted(); + // Drop the presentation state before destroying the viewer, so that + // document teardown is able to correctly persist the state. + SetContentViewer(nullptr); + SyncPresentationState(); + viewer->Destroy(); + } +} + +NS_IMETHODIMP +nsSHEntry::SetContentViewer(nsIContentViewer* aViewer) { + return GetState()->SetContentViewer(aViewer); +} + +NS_IMETHODIMP +nsSHEntry::GetContentViewer(nsIContentViewer** aResult) { + *aResult = GetState()->mContentViewer; + NS_IF_ADDREF(*aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetIsInBFCache(bool* aResult) { + *aResult = !!GetState()->mContentViewer; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::Clone(nsISHEntry** aResult) { + nsCOMPtr<nsISHEntry> entry = new nsSHEntry(*this); + entry.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetSaveLayoutStateFlag(bool* aFlag) { + *aFlag = mShared->mSaveLayoutState; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetSaveLayoutStateFlag(bool aFlag) { + mShared->mSaveLayoutState = aFlag; + if (mShared->mLayoutHistoryState) { + mShared->mLayoutHistoryState->SetScrollPositionOnly(!aFlag); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetWindowState(nsISupports* aState) { + GetState()->mWindowState = aState; + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetWindowState(nsISupports** aState) { + NS_IF_ADDREF(*aState = GetState()->mWindowState); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetRefreshURIList(nsIMutableArray** aList) { + NS_IF_ADDREF(*aList = GetState()->mRefreshURIList); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetRefreshURIList(nsIMutableArray* aList) { + GetState()->mRefreshURIList = aList; + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHEntry::AddChildShell(nsIDocShellTreeItem* aShell) { + MOZ_ASSERT(aShell, "Null child shell added to history entry"); + GetState()->mChildShells.AppendObject(aShell); +} + +NS_IMETHODIMP +nsSHEntry::ChildShellAt(int32_t aIndex, nsIDocShellTreeItem** aShell) { + NS_IF_ADDREF(*aShell = GetState()->mChildShells.SafeObjectAt(aIndex)); + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHEntry::ClearChildShells() { GetState()->mChildShells.Clear(); } + +NS_IMETHODIMP_(void) +nsSHEntry::SyncPresentationState() { GetState()->SyncPresentationState(); } + +nsDocShellEditorData* nsSHEntry::ForgetEditorData() { + // XXX jlebar Check how this is used. + return GetState()->mEditorData.release(); +} + +void nsSHEntry::SetEditorData(nsDocShellEditorData* aData) { + NS_ASSERTION(!(aData && GetState()->mEditorData), + "We're going to overwrite an owning ref!"); + if (GetState()->mEditorData != aData) { + GetState()->mEditorData = mozilla::WrapUnique(aData); + } +} + +bool nsSHEntry::HasDetachedEditor() { + return GetState()->mEditorData != nullptr; +} + +bool nsSHEntry::HasBFCacheEntry( + mozilla::dom::SHEntrySharedParentState* aEntry) { + return GetState() == aEntry; +} + +NS_IMETHODIMP +nsSHEntry::AbandonBFCacheEntry() { + mShared = GetState()->Duplicate(); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetBfcacheID(uint64_t* aBFCacheID) { + *aBFCacheID = mShared->GetId(); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetWireframe(JSContext* aCx, JS::MutableHandle<JS::Value> aOut) { + aOut.set(JS::NullValue()); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::SetWireframe(JSContext* aCx, JS::Handle<JS::Value> aArg) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/docshell/shistory/nsSHEntry.h b/docshell/shistory/nsSHEntry.h new file mode 100644 index 0000000000..eaa3cf1ad4 --- /dev/null +++ b/docshell/shistory/nsSHEntry.h @@ -0,0 +1,72 @@ +/* -*- 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/. */ + +#ifndef nsSHEntry_h +#define nsSHEntry_h + +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsISHEntry.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" + +class nsSHEntryShared; +class nsIInputStream; +class nsIURI; +class nsIReferrerInfo; + +class nsSHEntry : public nsISHEntry, public nsSupportsWeakReference { + public: + nsSHEntry(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISHENTRY + + virtual void EvictContentViewer(); + + static nsresult Startup(); + static void Shutdown(); + + nsSHEntryShared* GetState() { return mShared; } + + protected: + explicit nsSHEntry(const nsSHEntry& aOther); + virtual ~nsSHEntry(); + + // We share the state in here with other SHEntries which correspond to the + // same document. + RefPtr<nsSHEntryShared> mShared; + + // See nsSHEntry.idl for comments on these members. + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mOriginalURI; + nsCOMPtr<nsIURI> mResultPrincipalURI; + nsCOMPtr<nsIURI> mUnstrippedURI; + nsCOMPtr<nsIReferrerInfo> mReferrerInfo; + nsString mTitle; + nsString mName; + nsCOMPtr<nsIInputStream> mPostData; + uint32_t mLoadType; + uint32_t mID; + int32_t mScrollPositionX; + int32_t mScrollPositionY; + nsWeakPtr mParent; + nsCOMArray<nsISHEntry> mChildren; + nsCOMPtr<nsIStructuredCloneContainer> mStateData; + nsString mSrcdocData; + nsCOMPtr<nsIURI> mBaseURI; + bool mLoadReplace; + bool mURIWasModified; + bool mIsSrcdocEntry; + bool mScrollRestorationIsManual; + bool mLoadedInThisProcess; + bool mPersist; + bool mHasUserInteraction; + bool mHasUserActivation; +}; + +#endif /* nsSHEntry_h */ diff --git a/docshell/shistory/nsSHEntryShared.cpp b/docshell/shistory/nsSHEntryShared.cpp new file mode 100644 index 0000000000..9b8bc3936d --- /dev/null +++ b/docshell/shistory/nsSHEntryShared.cpp @@ -0,0 +1,343 @@ +/* -*- 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/. */ + +#include "nsSHEntryShared.h" + +#include "nsArray.h" +#include "nsContentUtils.h" +#include "nsDocShellEditorData.h" +#include "nsIContentViewer.h" +#include "nsISHistory.h" +#include "mozilla/dom/Document.h" +#include "nsILayoutHistoryState.h" +#include "nsIWebNavigation.h" +#include "nsSHistory.h" +#include "nsThreadUtils.h" +#include "nsFrameLoader.h" +#include "mozilla/Attributes.h" +#include "mozilla/Preferences.h" + +namespace dom = mozilla::dom; + +namespace { +uint64_t gSHEntrySharedID = 0; +nsTHashMap<nsUint64HashKey, mozilla::dom::SHEntrySharedParentState*>* + sIdToSharedState = nullptr; +} // namespace + +namespace mozilla { +namespace dom { + +/* static */ +uint64_t SHEntrySharedState::GenerateId() { + return nsContentUtils::GenerateProcessSpecificId(++gSHEntrySharedID); +} + +/* static */ +SHEntrySharedParentState* SHEntrySharedParentState::Lookup(uint64_t aId) { + MOZ_ASSERT(aId != 0); + + return sIdToSharedState ? sIdToSharedState->Get(aId) : nullptr; +} + +static void AddSHEntrySharedParentState( + SHEntrySharedParentState* aSharedState) { + MOZ_ASSERT(aSharedState->mId != 0); + + if (!sIdToSharedState) { + sIdToSharedState = + new nsTHashMap<nsUint64HashKey, SHEntrySharedParentState*>(); + } + sIdToSharedState->InsertOrUpdate(aSharedState->mId, aSharedState); +} + +SHEntrySharedParentState::SHEntrySharedParentState() { + AddSHEntrySharedParentState(this); +} + +SHEntrySharedParentState::SHEntrySharedParentState( + nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, const nsACString& aContentType) + : SHEntrySharedState(aTriggeringPrincipal, aPrincipalToInherit, + aPartitionedPrincipalToInherit, aCsp, aContentType) { + AddSHEntrySharedParentState(this); +} + +SHEntrySharedParentState::~SHEntrySharedParentState() { + MOZ_ASSERT(mId != 0); + + RefPtr<nsFrameLoader> loader = mFrameLoader; + SetFrameLoader(nullptr); + if (loader) { + if (NS_FAILED(NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "SHEntrySharedParentState::~SHEntrySharedParentState", + [loader]() -> void { loader->AsyncDestroy(); })))) { + // Trigger AsyncDestroy immediately during shutdown. + loader->AsyncDestroy(); + } + } + + sIdToSharedState->Remove(mId); + if (sIdToSharedState->IsEmpty()) { + delete sIdToSharedState; + sIdToSharedState = nullptr; + } +} + +void SHEntrySharedParentState::ChangeId(uint64_t aId) { + MOZ_ASSERT(aId != 0); + + sIdToSharedState->Remove(mId); + mId = aId; + sIdToSharedState->InsertOrUpdate(mId, this); +} + +void SHEntrySharedParentState::CopyFrom(SHEntrySharedParentState* aEntry) { + mDocShellID = aEntry->mDocShellID; + mTriggeringPrincipal = aEntry->mTriggeringPrincipal; + mPrincipalToInherit = aEntry->mPrincipalToInherit; + mPartitionedPrincipalToInherit = aEntry->mPartitionedPrincipalToInherit; + mCsp = aEntry->mCsp; + mSaveLayoutState = aEntry->mSaveLayoutState; + mContentType.Assign(aEntry->mContentType); + mIsFrameNavigation = aEntry->mIsFrameNavigation; + mSticky = aEntry->mSticky; + mDynamicallyCreated = aEntry->mDynamicallyCreated; + mCacheKey = aEntry->mCacheKey; + mLastTouched = aEntry->mLastTouched; +} + +void dom::SHEntrySharedParentState::NotifyListenersContentViewerEvicted() { + if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) { + RefPtr<nsSHistory> nsshistory = static_cast<nsSHistory*>(shistory.get()); + nsshistory->NotifyListenersContentViewerEvicted(1); + } +} + +void SHEntrySharedChildState::CopyFrom(SHEntrySharedChildState* aEntry) { + mChildShells.AppendObjects(aEntry->mChildShells); +} + +void SHEntrySharedParentState::SetFrameLoader(nsFrameLoader* aFrameLoader) { + // If expiration tracker is removing this object, IsTracked() returns false. + if (GetExpirationState()->IsTracked() && mFrameLoader) { + if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) { + shistory->RemoveFromExpirationTracker(this); + } + } + + mFrameLoader = aFrameLoader; + + if (mFrameLoader) { + if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) { + shistory->AddToExpirationTracker(this); + } + } +} + +nsFrameLoader* SHEntrySharedParentState::GetFrameLoader() { + return mFrameLoader; +} + +} // namespace dom +} // namespace mozilla + +void nsSHEntryShared::Shutdown() {} + +nsSHEntryShared::~nsSHEntryShared() { + // The destruction can be caused by either the entry is removed from session + // history and no one holds the reference, or the whole session history is on + // destruction. We want to ensure that we invoke + // shistory->RemoveFromExpirationTracker for the former case. + RemoveFromExpirationTracker(); + + // Calling RemoveDynEntriesForBFCacheEntry on destruction is unnecessary since + // there couldn't be any SHEntry holding this shared entry, and we noticed + // that calling RemoveDynEntriesForBFCacheEntry in the middle of + // nsSHistory::Release can cause a crash, so set mSHistory to null explicitly + // before RemoveFromBFCacheSync. + mSHistory = nullptr; + if (mContentViewer) { + RemoveFromBFCacheSync(); + } +} + +NS_IMPL_QUERY_INTERFACE(nsSHEntryShared, nsIBFCacheEntry, nsIMutationObserver) +NS_IMPL_ADDREF_INHERITED(nsSHEntryShared, dom::SHEntrySharedParentState) +NS_IMPL_RELEASE_INHERITED(nsSHEntryShared, dom::SHEntrySharedParentState) + +already_AddRefed<nsSHEntryShared> nsSHEntryShared::Duplicate() { + RefPtr<nsSHEntryShared> newEntry = new nsSHEntryShared(); + + newEntry->dom::SHEntrySharedParentState::CopyFrom(this); + newEntry->dom::SHEntrySharedChildState::CopyFrom(this); + + return newEntry.forget(); +} + +void nsSHEntryShared::RemoveFromExpirationTracker() { + nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory); + if (shistory && GetExpirationState()->IsTracked()) { + shistory->RemoveFromExpirationTracker(this); + } +} + +void nsSHEntryShared::SyncPresentationState() { + if (mContentViewer && mWindowState) { + // If we have a content viewer and a window state, we should be ok. + return; + } + + DropPresentationState(); +} + +void nsSHEntryShared::DropPresentationState() { + RefPtr<nsSHEntryShared> kungFuDeathGrip = this; + + if (mDocument) { + mDocument->SetBFCacheEntry(nullptr); + mDocument->RemoveMutationObserver(this); + mDocument = nullptr; + } + if (mContentViewer) { + mContentViewer->ClearHistoryEntry(); + } + + RemoveFromExpirationTracker(); + mContentViewer = nullptr; + mSticky = true; + mWindowState = nullptr; + mViewerBounds.SetRect(0, 0, 0, 0); + mChildShells.Clear(); + mRefreshURIList = nullptr; + mEditorData = nullptr; +} + +nsresult nsSHEntryShared::SetContentViewer(nsIContentViewer* aViewer) { + MOZ_ASSERT(!aViewer || !mContentViewer, + "SHEntryShared already contains viewer"); + + if (mContentViewer || !aViewer) { + DropPresentationState(); + } + + // If we're setting mContentViewer to null, state should already be cleared + // in the DropPresentationState() call above; If we're setting it to a + // non-null content viewer, the entry shouldn't have been tracked either. + MOZ_ASSERT(!GetExpirationState()->IsTracked()); + mContentViewer = aViewer; + + if (mContentViewer) { + // mSHistory is only set for root entries, but in general bfcache only + // applies to root entries as well. BFCache for subframe navigation has been + // disabled since 2005 in bug 304860. + if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) { + shistory->AddToExpirationTracker(this); + } + + // Store observed document in strong pointer in case it is removed from + // the contentviewer + mDocument = mContentViewer->GetDocument(); + if (mDocument) { + mDocument->SetBFCacheEntry(this); + mDocument->AddMutationObserver(this); + } + } + + return NS_OK; +} + +nsresult nsSHEntryShared::RemoveFromBFCacheSync() { + MOZ_ASSERT(mContentViewer && mDocument, "we're not in the bfcache!"); + + // The call to DropPresentationState could drop the last reference, so hold + // |this| until RemoveDynEntriesForBFCacheEntry finishes. + RefPtr<nsSHEntryShared> kungFuDeathGrip = this; + + // DropPresentationState would clear mContentViewer. + nsCOMPtr<nsIContentViewer> viewer = mContentViewer; + DropPresentationState(); + + if (viewer) { + viewer->Destroy(); + } + + // Now that we've dropped the viewer, we have to clear associated dynamic + // subframe entries. + nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory); + if (shistory) { + shistory->RemoveDynEntriesForBFCacheEntry(this); + } + + return NS_OK; +} + +nsresult nsSHEntryShared::RemoveFromBFCacheAsync() { + MOZ_ASSERT(mContentViewer && mDocument, "we're not in the bfcache!"); + + // Check it again to play safe in release builds. + if (!mDocument) { + return NS_ERROR_UNEXPECTED; + } + + // DropPresentationState would clear mContentViewer & mDocument. Capture and + // release the references asynchronously so that the document doesn't get + // nuked mid-mutation. + nsCOMPtr<nsIContentViewer> viewer = mContentViewer; + RefPtr<dom::Document> document = mDocument; + RefPtr<nsSHEntryShared> self = this; + nsresult rv = mDocument->Dispatch( + mozilla::TaskCategory::Other, + NS_NewRunnableFunction( + "nsSHEntryShared::RemoveFromBFCacheAsync", + [self, viewer, document]() { + if (viewer) { + viewer->Destroy(); + } + + nsCOMPtr<nsISHistory> shistory = do_QueryReferent(self->mSHistory); + if (shistory) { + shistory->RemoveDynEntriesForBFCacheEntry(self); + } + })); + + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch RemoveFromBFCacheAsync runnable."); + } else { + // Drop presentation. Only do this if we succeeded in posting the event + // since otherwise the document could be torn down mid-mutation, causing + // crashes. + DropPresentationState(); + } + + return NS_OK; +} + +void nsSHEntryShared::CharacterDataChanged(nsIContent* aContent, + const CharacterDataChangeInfo&) { + RemoveFromBFCacheAsync(); +} + +void nsSHEntryShared::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + RemoveFromBFCacheAsync(); +} + +void nsSHEntryShared::ContentAppended(nsIContent* aFirstNewContent) { + RemoveFromBFCacheAsync(); +} + +void nsSHEntryShared::ContentInserted(nsIContent* aChild) { + RemoveFromBFCacheAsync(); +} + +void nsSHEntryShared::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + RemoveFromBFCacheAsync(); +} diff --git a/docshell/shistory/nsSHEntryShared.h b/docshell/shistory/nsSHEntryShared.h new file mode 100644 index 0000000000..6bb6344202 --- /dev/null +++ b/docshell/shistory/nsSHEntryShared.h @@ -0,0 +1,219 @@ +/* -*- 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/. */ + +#ifndef nsSHEntryShared_h__ +#define nsSHEntryShared_h__ + +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsExpirationTracker.h" +#include "nsIBFCacheEntry.h" +#include "nsIWeakReferenceUtils.h" +#include "nsRect.h" +#include "nsString.h" +#include "nsStubMutationObserver.h" + +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" + +class nsSHEntry; +class nsISHEntry; +class nsISHistory; +class nsIContentSecurityPolicy; +class nsIContentViewer; +class nsIDocShellTreeItem; +class nsILayoutHistoryState; +class nsIPrincipal; +class nsDocShellEditorData; +class nsFrameLoader; +class nsIMutableArray; +class nsSHistory; + +// A document may have multiple SHEntries, either due to hash navigations or +// calls to history.pushState. SHEntries corresponding to the same document +// share many members; in particular, they share state related to the +// back/forward cache. +// +// The classes defined here are the vehicle for this sharing. +// +// Some of the state can only be stored in the process where we did the actual +// load, because that's where the objects live (eg. the content viewer). + +namespace mozilla { +namespace dom { +class Document; + +/** + * SHEntrySharedState holds shared state both in the child process and in the + * parent process. + */ +struct SHEntrySharedState { + SHEntrySharedState() : mId(GenerateId()) {} + SHEntrySharedState(const SHEntrySharedState& aState) = default; + SHEntrySharedState(nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, + const nsACString& aContentType) + : mId(GenerateId()), + mTriggeringPrincipal(aTriggeringPrincipal), + mPrincipalToInherit(aPrincipalToInherit), + mPartitionedPrincipalToInherit(aPartitionedPrincipalToInherit), + mCsp(aCsp), + mContentType(aContentType) {} + + // These members aren't copied by SHEntrySharedParentState::CopyFrom() because + // they're specific to a particular content viewer. + uint64_t mId = 0; + + // These members are copied by SHEntrySharedParentState::CopyFrom(). If you + // add a member here, be sure to update the CopyFrom() implementation. + nsCOMPtr<nsIPrincipal> mTriggeringPrincipal; + nsCOMPtr<nsIPrincipal> mPrincipalToInherit; + nsCOMPtr<nsIPrincipal> mPartitionedPrincipalToInherit; + nsCOMPtr<nsIContentSecurityPolicy> mCsp; + nsCString mContentType; + // Child side updates layout history state when page is being unloaded or + // moved to bfcache. + nsCOMPtr<nsILayoutHistoryState> mLayoutHistoryState; + uint32_t mCacheKey = 0; + bool mIsFrameNavigation = false; + bool mSaveLayoutState = true; + + protected: + static uint64_t GenerateId(); +}; + +/** + * SHEntrySharedParentState holds the shared state that can live in the parent + * process. + */ +class SHEntrySharedParentState : public SHEntrySharedState { + public: + friend class SessionHistoryInfo; + + uint64_t GetId() const { return mId; } + void ChangeId(uint64_t aId); + + void SetFrameLoader(nsFrameLoader* aFrameLoader); + + nsFrameLoader* GetFrameLoader(); + + void NotifyListenersContentViewerEvicted(); + + nsExpirationState* GetExpirationState() { return &mExpirationState; } + + SHEntrySharedParentState(); + SHEntrySharedParentState(nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, + const nsACString& aContentType); + + // This returns the existing SHEntrySharedParentState that was registered for + // aId, if one exists. + static SHEntrySharedParentState* Lookup(uint64_t aId); + + protected: + virtual ~SHEntrySharedParentState(); + NS_INLINE_DECL_VIRTUAL_REFCOUNTING_WITH_DESTROY(SHEntrySharedParentState, + Destroy()) + + virtual void Destroy() { delete this; } + + void CopyFrom(SHEntrySharedParentState* aSource); + + // These members are copied by SHEntrySharedParentState::CopyFrom(). If you + // add a member here, be sure to update the CopyFrom() implementation. + nsID mDocShellID{}; + + nsIntRect mViewerBounds{0, 0, 0, 0}; + + uint32_t mLastTouched = 0; + + // These members aren't copied by SHEntrySharedParentState::CopyFrom() because + // they're specific to a particular content viewer. + nsWeakPtr mSHistory; + + RefPtr<nsFrameLoader> mFrameLoader; + + nsExpirationState mExpirationState; + + bool mSticky = true; + bool mDynamicallyCreated = false; + + // This flag is about necko cache, not bfcache. + bool mExpired = false; +}; + +/** + * SHEntrySharedChildState holds the shared state that needs to live in the + * process where the document was loaded. + */ +class SHEntrySharedChildState { + protected: + void CopyFrom(SHEntrySharedChildState* aSource); + + public: + // These members are copied by SHEntrySharedChildState::CopyFrom(). If you + // add a member here, be sure to update the CopyFrom() implementation. + nsCOMArray<nsIDocShellTreeItem> mChildShells; + + // These members aren't copied by SHEntrySharedChildState::CopyFrom() because + // they're specific to a particular content viewer. + nsCOMPtr<nsIContentViewer> mContentViewer; + RefPtr<mozilla::dom::Document> mDocument; + nsCOMPtr<nsISupports> mWindowState; + // FIXME Move to parent? + nsCOMPtr<nsIMutableArray> mRefreshURIList; + UniquePtr<nsDocShellEditorData> mEditorData; +}; + +} // namespace dom +} // namespace mozilla + +/** + * nsSHEntryShared holds the shared state if the session history is not stored + * in the parent process, or if the load itself happens in the parent process. + * Note, since nsSHEntryShared inherits both SHEntrySharedParentState and + * SHEntrySharedChildState and those have some same member variables, + * the ones from SHEntrySharedParentState should be used. + */ +class nsSHEntryShared final : public nsIBFCacheEntry, + public nsStubMutationObserver, + public mozilla::dom::SHEntrySharedParentState, + public mozilla::dom::SHEntrySharedChildState { + public: + static void EnsureHistoryTracker(); + static void Shutdown(); + + using SHEntrySharedParentState::SHEntrySharedParentState; + + already_AddRefed<nsSHEntryShared> Duplicate(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIBFCACHEENTRY + + // The nsIMutationObserver bits we actually care about. + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + private: + ~nsSHEntryShared(); + + friend class nsSHEntry; + + void RemoveFromExpirationTracker(); + void SyncPresentationState(); + void DropPresentationState(); + + nsresult SetContentViewer(nsIContentViewer* aViewer); +}; + +#endif diff --git a/docshell/shistory/nsSHistory.cpp b/docshell/shistory/nsSHistory.cpp new file mode 100644 index 0000000000..214646dde9 --- /dev/null +++ b/docshell/shistory/nsSHistory.cpp @@ -0,0 +1,2410 @@ +/* -*- 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/. */ + +#include "nsSHistory.h" + +#include <algorithm> + +#include "nsContentUtils.h" +#include "nsCOMArray.h" +#include "nsComponentManagerUtils.h" +#include "nsDocShell.h" +#include "nsFrameLoaderOwner.h" +#include "nsHashKeys.h" +#include "nsIContentViewer.h" +#include "nsIDocShell.h" +#include "nsDocShellLoadState.h" +#include "nsIDocShellTreeItem.h" +#include "nsILayoutHistoryState.h" +#include "nsIObserverService.h" +#include "nsISHEntry.h" +#include "nsISHistoryListener.h" +#include "nsIURI.h" +#include "nsIXULRuntime.h" +#include "nsNetUtil.h" +#include "nsTHashMap.h" +#include "nsSHEntry.h" +#include "SessionHistoryEntry.h" +#include "nsTArray.h" +#include "prsystem.h" + +#include "mozilla/Attributes.h" +#include "mozilla/dom/BrowsingContextGroup.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/RemoteWebProgressRequest.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/LinkedList.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProcessPriorityManager.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "nsIWebNavigation.h" +#include "nsDocShellLoadTypes.h" +#include "base/process.h" + +using namespace mozilla; +using namespace mozilla::dom; + +#define PREF_SHISTORY_SIZE "browser.sessionhistory.max_entries" +#define PREF_SHISTORY_MAX_TOTAL_VIEWERS \ + "browser.sessionhistory.max_total_viewers" +#define CONTENT_VIEWER_TIMEOUT_SECONDS \ + "browser.sessionhistory.contentViewerTimeout" +// Observe fission.bfcacheInParent so that BFCache can be enabled/disabled when +// the pref is changed. +#define PREF_FISSION_BFCACHEINPARENT "fission.bfcacheInParent" + +// Default this to time out unused content viewers after 30 minutes +#define CONTENT_VIEWER_TIMEOUT_SECONDS_DEFAULT (30 * 60) + +static const char* kObservedPrefs[] = {PREF_SHISTORY_SIZE, + PREF_SHISTORY_MAX_TOTAL_VIEWERS, + PREF_FISSION_BFCACHEINPARENT, nullptr}; + +static int32_t gHistoryMaxSize = 50; + +// List of all SHistory objects, used for content viewer cache eviction. +// When being destroyed, this helper removes everything from the list to avoid +// assertions when we leak. +struct ListHelper { +#ifdef DEBUG + ~ListHelper() { mList.clear(); } +#endif // DEBUG + + LinkedList<nsSHistory> mList; +}; + +static ListHelper gSHistoryList; +// Max viewers allowed total, across all SHistory objects - negative default +// means we will calculate how many viewers to cache based on total memory +int32_t nsSHistory::sHistoryMaxTotalViewers = -1; + +// A counter that is used to be able to know the order in which +// entries were touched, so that we can evict older entries first. +static uint32_t gTouchCounter = 0; + +extern mozilla::LazyLogModule gSHLog; + +LazyLogModule gSHistoryLog("nsSHistory"); + +#define LOG(format) MOZ_LOG(gSHistoryLog, mozilla::LogLevel::Debug, format) + +extern mozilla::LazyLogModule gPageCacheLog; +extern mozilla::LazyLogModule gSHIPBFCacheLog; + +// This macro makes it easier to print a log message which includes a URI's +// spec. Example use: +// +// nsIURI *uri = [...]; +// LOG_SPEC(("The URI is %s.", _spec), uri); +// +#define LOG_SPEC(format, uri) \ + PR_BEGIN_MACRO \ + if (MOZ_LOG_TEST(gSHistoryLog, LogLevel::Debug)) { \ + nsAutoCString _specStr("(null)"_ns); \ + if (uri) { \ + _specStr = uri->GetSpecOrDefault(); \ + } \ + const char* _spec = _specStr.get(); \ + LOG(format); \ + } \ + PR_END_MACRO + +// This macro makes it easy to log a message including an SHEntry's URI. +// For example: +// +// nsCOMPtr<nsISHEntry> shentry = [...]; +// LOG_SHENTRY_SPEC(("shentry %p has uri %s.", shentry.get(), _spec), shentry); +// +#define LOG_SHENTRY_SPEC(format, shentry) \ + PR_BEGIN_MACRO \ + if (MOZ_LOG_TEST(gSHistoryLog, LogLevel::Debug)) { \ + nsCOMPtr<nsIURI> uri = shentry->GetURI(); \ + LOG_SPEC(format, uri); \ + } \ + PR_END_MACRO + +// Calls a F on all registered session history listeners. +template <typename F> +static void NotifyListeners(nsAutoTObserverArray<nsWeakPtr, 2>& aListeners, + F&& f) { + for (const nsWeakPtr& weakPtr : aListeners.EndLimitedRange()) { + nsCOMPtr<nsISHistoryListener> listener = do_QueryReferent(weakPtr); + if (listener) { + f(listener); + } + } +} + +class MOZ_STACK_CLASS SHistoryChangeNotifier { + public: + explicit SHistoryChangeNotifier(nsSHistory* aHistory) { + // If we're already in an update, the outermost change notifier will + // update browsing context in the destructor. + if (!aHistory->HasOngoingUpdate()) { + aHistory->SetHasOngoingUpdate(true); + mSHistory = aHistory; + } + } + + ~SHistoryChangeNotifier() { + if (mSHistory) { + MOZ_ASSERT(mSHistory->HasOngoingUpdate()); + mSHistory->SetHasOngoingUpdate(false); + + RefPtr<BrowsingContext> rootBC = mSHistory->GetBrowsingContext(); + if (mozilla::SessionHistoryInParent() && rootBC) { + rootBC->Canonical()->HistoryCommitIndexAndLength(); + } + } + } + + RefPtr<nsSHistory> mSHistory; +}; + +enum HistCmd { HIST_CMD_GOTOINDEX, HIST_CMD_RELOAD }; + +class nsSHistoryObserver final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + nsSHistoryObserver() {} + + static void PrefChanged(const char* aPref, void* aSelf); + void PrefChanged(const char* aPref); + + protected: + ~nsSHistoryObserver() {} +}; + +StaticRefPtr<nsSHistoryObserver> gObserver; + +NS_IMPL_ISUPPORTS(nsSHistoryObserver, nsIObserver) + +// static +void nsSHistoryObserver::PrefChanged(const char* aPref, void* aSelf) { + static_cast<nsSHistoryObserver*>(aSelf)->PrefChanged(aPref); +} + +void nsSHistoryObserver::PrefChanged(const char* aPref) { + nsSHistory::UpdatePrefs(); + nsSHistory::GloballyEvictContentViewers(); +} + +NS_IMETHODIMP +nsSHistoryObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "cacheservice:empty-cache") || + !strcmp(aTopic, "memory-pressure")) { + nsSHistory::GloballyEvictAllContentViewers(); + } + + return NS_OK; +} + +void nsSHistory::EvictContentViewerForEntry(nsISHEntry* aEntry) { + nsCOMPtr<nsIContentViewer> viewer = aEntry->GetContentViewer(); + if (viewer) { + LOG_SHENTRY_SPEC(("Evicting content viewer 0x%p for " + "owning SHEntry 0x%p at %s.", + viewer.get(), aEntry, _spec), + aEntry); + + // Drop the presentation state before destroying the viewer, so that + // document teardown is able to correctly persist the state. + NotifyListenersContentViewerEvicted(1); + aEntry->SetContentViewer(nullptr); + aEntry->SyncPresentationState(); + viewer->Destroy(); + } else if (nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aEntry)) { + if (RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader()) { + nsCOMPtr<nsFrameLoaderOwner> owner = + do_QueryInterface(frameLoader->GetOwnerContent()); + RefPtr<nsFrameLoader> currentFrameLoader; + if (owner) { + currentFrameLoader = owner->GetFrameLoader(); + } + + // Only destroy non-current frameloader when evicting from the bfcache. + if (currentFrameLoader != frameLoader) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, + ("nsSHistory::EvictContentViewerForEntry " + "destroying an nsFrameLoader.")); + NotifyListenersContentViewerEvicted(1); + she->SetFrameLoader(nullptr); + frameLoader->Destroy(); + } + } + } + + // When dropping bfcache, we have to remove associated dynamic entries as + // well. + int32_t index = GetIndexOfEntry(aEntry); + if (index != -1) { + RemoveDynEntries(index, aEntry); + } +} + +nsSHistory::nsSHistory(BrowsingContext* aRootBC) + : mRootBC(aRootBC->Id()), + mHasOngoingUpdate(false), + mIndex(-1), + mRequestedIndex(-1), + mRootDocShellID(aRootBC->GetHistoryID()) { + static bool sCalledStartup = false; + if (!sCalledStartup) { + Startup(); + sCalledStartup = true; + } + + // Add this new SHistory object to the list + gSHistoryList.mList.insertBack(this); + + // Init mHistoryTracker on setting mRootBC so we can bind its event + // target to the tabGroup. + mHistoryTracker = mozilla::MakeUnique<HistoryTracker>( + this, + mozilla::Preferences::GetUint(CONTENT_VIEWER_TIMEOUT_SECONDS, + CONTENT_VIEWER_TIMEOUT_SECONDS_DEFAULT), + GetCurrentSerialEventTarget()); +} + +nsSHistory::~nsSHistory() { + // Clear mEntries explicitly here so that the destructor of the entries + // can still access nsSHistory in a reasonable way. + mEntries.Clear(); +} + +NS_IMPL_ADDREF(nsSHistory) +NS_IMPL_RELEASE(nsSHistory) + +NS_INTERFACE_MAP_BEGIN(nsSHistory) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISHistory) + NS_INTERFACE_MAP_ENTRY(nsISHistory) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +// static +uint32_t nsSHistory::CalcMaxTotalViewers() { +// This value allows tweaking how fast the allowed amount of content viewers +// grows with increasing amounts of memory. Larger values mean slower growth. +#ifdef ANDROID +# define MAX_TOTAL_VIEWERS_BIAS 15.9 +#else +# define MAX_TOTAL_VIEWERS_BIAS 14 +#endif + + // Calculate an estimate of how many ContentViewers we should cache based + // on RAM. This assumes that the average ContentViewer is 4MB (conservative) + // and caps the max at 8 ContentViewers + // + // TODO: Should we split the cache memory betw. ContentViewer caching and + // nsCacheService? + // + // RAM | ContentViewers | on Android + // ------------------------------------- + // 32 Mb 0 0 + // 64 Mb 1 0 + // 128 Mb 2 0 + // 256 Mb 3 1 + // 512 Mb 5 2 + // 768 Mb 6 2 + // 1024 Mb 8 3 + // 2048 Mb 8 5 + // 3072 Mb 8 7 + // 4096 Mb 8 8 + uint64_t bytes = PR_GetPhysicalMemorySize(); + + if (bytes == 0) { + return 0; + } + + // Conversion from unsigned int64_t to double doesn't work on all platforms. + // We need to truncate the value at INT64_MAX to make sure we don't + // overflow. + if (bytes > INT64_MAX) { + bytes = INT64_MAX; + } + + double kBytesD = (double)(bytes >> 10); + + // This is essentially the same calculation as for nsCacheService, + // except that we divide the final memory calculation by 4, since + // we assume each ContentViewer takes on average 4MB + uint32_t viewers = 0; + double x = std::log(kBytesD) / std::log(2.0) - MAX_TOTAL_VIEWERS_BIAS; + if (x > 0) { + viewers = (uint32_t)(x * x - x + 2.001); // add .001 for rounding + viewers /= 4; + } + + // Cap it off at 8 max + if (viewers > 8) { + viewers = 8; + } + return viewers; +} + +// static +void nsSHistory::UpdatePrefs() { + Preferences::GetInt(PREF_SHISTORY_SIZE, &gHistoryMaxSize); + if (mozilla::SessionHistoryInParent() && !mozilla::BFCacheInParent()) { + sHistoryMaxTotalViewers = 0; + return; + } + + Preferences::GetInt(PREF_SHISTORY_MAX_TOTAL_VIEWERS, + &sHistoryMaxTotalViewers); + // If the pref is negative, that means we calculate how many viewers + // we think we should cache, based on total memory + if (sHistoryMaxTotalViewers < 0) { + sHistoryMaxTotalViewers = CalcMaxTotalViewers(); + } +} + +// static +nsresult nsSHistory::Startup() { + UpdatePrefs(); + + // The goal of this is to unbreak users who have inadvertently set their + // session history size to less than the default value. + int32_t defaultHistoryMaxSize = + Preferences::GetInt(PREF_SHISTORY_SIZE, 50, PrefValueKind::Default); + if (gHistoryMaxSize < defaultHistoryMaxSize) { + gHistoryMaxSize = defaultHistoryMaxSize; + } + + // Allow the user to override the max total number of cached viewers, + // but keep the per SHistory cached viewer limit constant + if (!gObserver) { + gObserver = new nsSHistoryObserver(); + Preferences::RegisterCallbacks(nsSHistoryObserver::PrefChanged, + kObservedPrefs, gObserver.get()); + + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + if (obsSvc) { + // Observe empty-cache notifications so tahat clearing the disk/memory + // cache will also evict all content viewers. + obsSvc->AddObserver(gObserver, "cacheservice:empty-cache", false); + + // Same for memory-pressure notifications + obsSvc->AddObserver(gObserver, "memory-pressure", false); + } + } + + return NS_OK; +} + +// static +void nsSHistory::Shutdown() { + if (gObserver) { + Preferences::UnregisterCallbacks(nsSHistoryObserver::PrefChanged, + kObservedPrefs, gObserver.get()); + + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + if (obsSvc) { + obsSvc->RemoveObserver(gObserver, "cacheservice:empty-cache"); + obsSvc->RemoveObserver(gObserver, "memory-pressure"); + } + gObserver = nullptr; + } +} + +// static +already_AddRefed<nsISHEntry> nsSHistory::GetRootSHEntry(nsISHEntry* aEntry) { + nsCOMPtr<nsISHEntry> rootEntry = aEntry; + nsCOMPtr<nsISHEntry> result = nullptr; + while (rootEntry) { + result = rootEntry; + rootEntry = result->GetParent(); + } + + return result.forget(); +} + +// static +nsresult nsSHistory::WalkHistoryEntries(nsISHEntry* aRootEntry, + BrowsingContext* aBC, + WalkHistoryEntriesFunc aCallback, + void* aData) { + NS_ENSURE_TRUE(aRootEntry, NS_ERROR_FAILURE); + + int32_t childCount = aRootEntry->GetChildCount(); + for (int32_t i = 0; i < childCount; i++) { + nsCOMPtr<nsISHEntry> childEntry; + aRootEntry->GetChildAt(i, getter_AddRefs(childEntry)); + if (!childEntry) { + // childEntry can be null for valid reasons, for example if the + // docshell at index i never loaded anything useful. + // Remember to clone also nulls in the child array (bug 464064). + aCallback(nullptr, nullptr, i, aData); + continue; + } + + BrowsingContext* childBC = nullptr; + if (aBC) { + for (BrowsingContext* child : aBC->Children()) { + // If the SH pref is on and we are in the parent process, update + // canonical BC directly + bool foundChild = false; + if (mozilla::SessionHistoryInParent() && XRE_IsParentProcess()) { + if (child->Canonical()->HasHistoryEntry(childEntry)) { + childBC = child; + foundChild = true; + } + } + + nsDocShell* docshell = static_cast<nsDocShell*>(child->GetDocShell()); + if (docshell && docshell->HasHistoryEntry(childEntry)) { + childBC = docshell->GetBrowsingContext(); + foundChild = true; + } + + // XXX Simplify this once the old and new session history + // implementations don't run at the same time. + if (foundChild) { + break; + } + } + } + + nsresult rv = aCallback(childEntry, childBC, i, aData); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// callback data for WalkHistoryEntries +struct MOZ_STACK_CLASS CloneAndReplaceData { + CloneAndReplaceData(uint32_t aCloneID, nsISHEntry* aReplaceEntry, + bool aCloneChildren, nsISHEntry* aDestTreeParent) + : cloneID(aCloneID), + cloneChildren(aCloneChildren), + replaceEntry(aReplaceEntry), + destTreeParent(aDestTreeParent) {} + + uint32_t cloneID; + bool cloneChildren; + nsISHEntry* replaceEntry; + nsISHEntry* destTreeParent; + nsCOMPtr<nsISHEntry> resultEntry; +}; + +nsresult nsSHistory::CloneAndReplaceChild(nsISHEntry* aEntry, + BrowsingContext* aOwnerBC, + int32_t aChildIndex, void* aData) { + nsCOMPtr<nsISHEntry> dest; + + CloneAndReplaceData* data = static_cast<CloneAndReplaceData*>(aData); + uint32_t cloneID = data->cloneID; + nsISHEntry* replaceEntry = data->replaceEntry; + + if (!aEntry) { + if (data->destTreeParent) { + data->destTreeParent->AddChild(nullptr, aChildIndex); + } + return NS_OK; + } + + uint32_t srcID = aEntry->GetID(); + + nsresult rv = NS_OK; + if (srcID == cloneID) { + // Replace the entry + dest = replaceEntry; + } else { + // Clone the SHEntry... + rv = aEntry->Clone(getter_AddRefs(dest)); + NS_ENSURE_SUCCESS(rv, rv); + } + dest->SetIsSubFrame(true); + + if (srcID != cloneID || data->cloneChildren) { + // Walk the children + CloneAndReplaceData childData(cloneID, replaceEntry, data->cloneChildren, + dest); + rv = nsSHistory::WalkHistoryEntries(aEntry, aOwnerBC, CloneAndReplaceChild, + &childData); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (srcID != cloneID && aOwnerBC) { + nsSHistory::HandleEntriesToSwapInDocShell(aOwnerBC, aEntry, dest); + } + + if (data->destTreeParent) { + data->destTreeParent->AddChild(dest, aChildIndex); + } + data->resultEntry = dest; + return rv; +} + +// static +nsresult nsSHistory::CloneAndReplace( + nsISHEntry* aSrcEntry, BrowsingContext* aOwnerBC, uint32_t aCloneID, + nsISHEntry* aReplaceEntry, bool aCloneChildren, nsISHEntry** aDestEntry) { + NS_ENSURE_ARG_POINTER(aDestEntry); + NS_ENSURE_TRUE(aReplaceEntry, NS_ERROR_FAILURE); + CloneAndReplaceData data(aCloneID, aReplaceEntry, aCloneChildren, nullptr); + nsresult rv = CloneAndReplaceChild(aSrcEntry, aOwnerBC, 0, &data); + data.resultEntry.swap(*aDestEntry); + return rv; +} + +// static +void nsSHistory::WalkContiguousEntries( + nsISHEntry* aEntry, const std::function<void(nsISHEntry*)>& aCallback) { + MOZ_ASSERT(aEntry); + + nsCOMPtr<nsISHistory> shistory = aEntry->GetShistory(); + if (!shistory) { + // If there is no session history in the entry, it means this is not a root + // entry. So, we can return from here. + return; + } + + int32_t index = shistory->GetIndexOfEntry(aEntry); + int32_t count = shistory->GetCount(); + + nsCOMPtr<nsIURI> targetURI = aEntry->GetURI(); + + // First, call the callback on the input entry. + aCallback(aEntry); + + // Walk backward to find the entries that have the same origin as the + // input entry. + for (int32_t i = index - 1; i >= 0; i--) { + RefPtr<nsISHEntry> entry; + shistory->GetEntryAtIndex(i, getter_AddRefs(entry)); + if (entry) { + nsCOMPtr<nsIURI> uri = entry->GetURI(); + if (NS_FAILED(nsContentUtils::GetSecurityManager()->CheckSameOriginURI( + targetURI, uri, false, false))) { + break; + } + + aCallback(entry); + } + } + + // Then, Walk forward. + for (int32_t i = index + 1; i < count; i++) { + RefPtr<nsISHEntry> entry; + shistory->GetEntryAtIndex(i, getter_AddRefs(entry)); + if (entry) { + nsCOMPtr<nsIURI> uri = entry->GetURI(); + if (NS_FAILED(nsContentUtils::GetSecurityManager()->CheckSameOriginURI( + targetURI, uri, false, false))) { + break; + } + + aCallback(entry); + } + } +} + +NS_IMETHODIMP +nsSHistory::AddChildSHEntryHelper(nsISHEntry* aCloneRef, nsISHEntry* aNewEntry, + BrowsingContext* aRootBC, + bool aCloneChildren) { + MOZ_ASSERT(aRootBC->IsTop()); + + /* You are currently in the rootDocShell. + * You will get here when a subframe has a new url + * to load and you have walked up the tree all the + * way to the top to clone the current SHEntry hierarchy + * and replace the subframe where a new url was loaded with + * a new entry. + */ + nsCOMPtr<nsISHEntry> child; + nsCOMPtr<nsISHEntry> currentHE; + int32_t index = mIndex; + if (index < 0) { + return NS_ERROR_FAILURE; + } + + GetEntryAtIndex(index, getter_AddRefs(currentHE)); + NS_ENSURE_TRUE(currentHE, NS_ERROR_FAILURE); + + nsresult rv = NS_OK; + uint32_t cloneID = aCloneRef->GetID(); + rv = nsSHistory::CloneAndReplace(currentHE, aRootBC, cloneID, aNewEntry, + aCloneChildren, getter_AddRefs(child)); + + if (NS_SUCCEEDED(rv)) { + rv = AddEntry(child, true); + if (NS_SUCCEEDED(rv)) { + child->SetDocshellID(aRootBC->GetHistoryID()); + } + } + + return rv; +} + +nsresult nsSHistory::SetChildHistoryEntry(nsISHEntry* aEntry, + BrowsingContext* aBC, + int32_t aEntryIndex, void* aData) { + SwapEntriesData* data = static_cast<SwapEntriesData*>(aData); + if (!aBC || aBC == data->ignoreBC) { + return NS_OK; + } + + nsISHEntry* destTreeRoot = data->destTreeRoot; + + nsCOMPtr<nsISHEntry> destEntry; + + if (data->destTreeParent) { + // aEntry is a clone of some child of destTreeParent, but since the + // trees aren't necessarily in sync, we'll have to locate it. + // Note that we could set aShell's entry to null if we don't find a + // corresponding entry under destTreeParent. + + uint32_t targetID = aEntry->GetID(); + + // First look at the given index, since this is the common case. + nsCOMPtr<nsISHEntry> entry; + data->destTreeParent->GetChildAt(aEntryIndex, getter_AddRefs(entry)); + if (entry && entry->GetID() == targetID) { + destEntry.swap(entry); + } else { + int32_t childCount; + data->destTreeParent->GetChildCount(&childCount); + for (int32_t i = 0; i < childCount; ++i) { + data->destTreeParent->GetChildAt(i, getter_AddRefs(entry)); + if (!entry) { + continue; + } + + if (entry->GetID() == targetID) { + destEntry.swap(entry); + break; + } + } + } + } else { + destEntry = destTreeRoot; + } + + nsSHistory::HandleEntriesToSwapInDocShell(aBC, aEntry, destEntry); + // Now handle the children of aEntry. + SwapEntriesData childData = {data->ignoreBC, destTreeRoot, destEntry}; + return nsSHistory::WalkHistoryEntries(aEntry, aBC, SetChildHistoryEntry, + &childData); +} + +// static +void nsSHistory::HandleEntriesToSwapInDocShell( + mozilla::dom::BrowsingContext* aBC, nsISHEntry* aOldEntry, + nsISHEntry* aNewEntry) { + bool shPref = mozilla::SessionHistoryInParent(); + if (aBC->IsInProcess() || !shPref) { + nsDocShell* docshell = static_cast<nsDocShell*>(aBC->GetDocShell()); + if (docshell) { + docshell->SwapHistoryEntries(aOldEntry, aNewEntry); + } + } else { + // FIXME Bug 1633988: Need to update entries? + } + + // XXX Simplify this once the old and new session history implementations + // don't run at the same time. + if (shPref && XRE_IsParentProcess()) { + aBC->Canonical()->SwapHistoryEntries(aOldEntry, aNewEntry); + } +} + +void nsSHistory::UpdateRootBrowsingContextState(BrowsingContext* aRootBC) { + if (aRootBC && aRootBC->EverAttached()) { + bool sameDocument = IsEmptyOrHasEntriesForSingleTopLevelPage(); + if (sameDocument != aRootBC->GetIsSingleToplevelInHistory()) { + // If the browsing context is discarded then its session history is + // invalid and will go away. + Unused << aRootBC->SetIsSingleToplevelInHistory(sameDocument); + } + } +} + +NS_IMETHODIMP +nsSHistory::AddToRootSessionHistory(bool aCloneChildren, nsISHEntry* aOSHE, + BrowsingContext* aRootBC, + nsISHEntry* aEntry, uint32_t aLoadType, + bool aShouldPersist, + Maybe<int32_t>* aPreviousEntryIndex, + Maybe<int32_t>* aLoadedEntryIndex) { + MOZ_ASSERT(aRootBC->IsTop()); + + nsresult rv = NS_OK; + + // If we need to clone our children onto the new session + // history entry, do so now. + if (aCloneChildren && aOSHE) { + uint32_t cloneID = aOSHE->GetID(); + nsCOMPtr<nsISHEntry> newEntry; + nsSHistory::CloneAndReplace(aOSHE, aRootBC, cloneID, aEntry, true, + getter_AddRefs(newEntry)); + NS_ASSERTION(aEntry == newEntry, + "The new session history should be in the new entry"); + } + // This is the root docshell + bool addToSHistory = !LOAD_TYPE_HAS_FLAGS( + aLoadType, nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY); + if (!addToSHistory) { + // Replace current entry in session history; If the requested index is + // valid, it indicates the loading was triggered by a history load, and + // we should replace the entry at requested index instead. + int32_t index = GetIndexForReplace(); + + // Replace the current entry with the new entry + if (index >= 0) { + rv = ReplaceEntry(index, aEntry); + } else { + // If we're trying to replace an inexistant shistory entry, append. + addToSHistory = true; + } + } + if (addToSHistory) { + // Add to session history + *aPreviousEntryIndex = Some(mIndex); + rv = AddEntry(aEntry, aShouldPersist); + *aLoadedEntryIndex = Some(mIndex); + MOZ_LOG(gPageCacheLog, LogLevel::Verbose, + ("Previous index: %d, Loaded index: %d", + aPreviousEntryIndex->value(), aLoadedEntryIndex->value())); + } + if (NS_SUCCEEDED(rv)) { + aEntry->SetDocshellID(aRootBC->GetHistoryID()); + } + return rv; +} + +/* Add an entry to the History list at mIndex and + * increment the index to point to the new entry + */ +NS_IMETHODIMP +nsSHistory::AddEntry(nsISHEntry* aSHEntry, bool aPersist) { + NS_ENSURE_ARG(aSHEntry); + + nsCOMPtr<nsISHistory> shistoryOfEntry = aSHEntry->GetShistory(); + if (shistoryOfEntry && shistoryOfEntry != this) { + NS_WARNING( + "The entry has been associated to another nsISHistory instance. " + "Try nsISHEntry.clone() and nsISHEntry.abandonBFCacheEntry() " + "first if you're copying an entry from another nsISHistory."); + return NS_ERROR_FAILURE; + } + + aSHEntry->SetShistory(this); + + // If we have a root docshell, update the docshell id of the root shentry to + // match the id of that docshell + RefPtr<BrowsingContext> rootBC = GetBrowsingContext(); + if (rootBC) { + aSHEntry->SetDocshellID(mRootDocShellID); + } + + if (mIndex >= 0) { + MOZ_ASSERT(mIndex < Length(), "Index out of range!"); + if (mIndex >= Length()) { + return NS_ERROR_FAILURE; + } + + if (mEntries[mIndex] && !mEntries[mIndex]->GetPersist()) { + NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); }); + aSHEntry->SetPersist(aPersist); + mEntries[mIndex] = aSHEntry; + UpdateRootBrowsingContextState(); + return NS_OK; + } + } + SHistoryChangeNotifier change(this); + + int32_t truncating = Length() - 1 - mIndex; + if (truncating > 0) { + NotifyListeners(mListeners, + [truncating](auto l) { l->OnHistoryTruncate(truncating); }); + } + + nsCOMPtr<nsIURI> uri = aSHEntry->GetURI(); + NotifyListeners(mListeners, + [&uri, this](auto l) { l->OnHistoryNewEntry(uri, mIndex); }); + + // Remove all entries after the current one, add the new one, and set the + // new one as the current one. + MOZ_ASSERT(mIndex >= -1); + aSHEntry->SetPersist(aPersist); + mEntries.TruncateLength(mIndex + 1); + mEntries.AppendElement(aSHEntry); + mIndex++; + if (mIndex > 0) { + UpdateEntryLength(mEntries[mIndex - 1], mEntries[mIndex], false); + } + + // Purge History list if it is too long + if (gHistoryMaxSize >= 0 && Length() > gHistoryMaxSize) { + PurgeHistory(Length() - gHistoryMaxSize); + } + + UpdateRootBrowsingContextState(); + + return NS_OK; +} + +void nsSHistory::NotifyOnHistoryReplaceEntry() { + NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); }); +} + +/* Get size of the history list */ +NS_IMETHODIMP +nsSHistory::GetCount(int32_t* aResult) { + MOZ_ASSERT(aResult, "null out param?"); + *aResult = Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsSHistory::GetIndex(int32_t* aResult) { + MOZ_ASSERT(aResult, "null out param?"); + *aResult = mIndex; + return NS_OK; +} + +NS_IMETHODIMP +nsSHistory::SetIndex(int32_t aIndex) { + if (aIndex < 0 || aIndex >= Length()) { + return NS_ERROR_FAILURE; + } + + mIndex = aIndex; + return NS_OK; +} + +/* Get the requestedIndex */ +NS_IMETHODIMP +nsSHistory::GetRequestedIndex(int32_t* aResult) { + MOZ_ASSERT(aResult, "null out param?"); + *aResult = mRequestedIndex; + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHistory::InternalSetRequestedIndex(int32_t aRequestedIndex) { + MOZ_ASSERT(aRequestedIndex >= -1 && aRequestedIndex < Length()); + mRequestedIndex = aRequestedIndex; +} + +NS_IMETHODIMP +nsSHistory::GetEntryAtIndex(int32_t aIndex, nsISHEntry** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (aIndex < 0 || aIndex >= Length()) { + return NS_ERROR_FAILURE; + } + + *aResult = mEntries[aIndex]; + NS_ADDREF(*aResult); + return NS_OK; +} + +NS_IMETHODIMP_(int32_t) +nsSHistory::GetIndexOfEntry(nsISHEntry* aSHEntry) { + for (int32_t i = 0; i < Length(); i++) { + if (aSHEntry == mEntries[i]) { + return i; + } + } + + return -1; +} + +static void LogEntry(nsISHEntry* aEntry, int32_t aIndex, int32_t aTotal, + const nsCString& aPrefix, bool aIsCurrent) { + if (!aEntry) { + MOZ_LOG(gSHLog, LogLevel::Debug, + (" %s+- %i SH Entry null\n", aPrefix.get(), aIndex)); + return; + } + + nsCOMPtr<nsIURI> uri = aEntry->GetURI(); + nsAutoString title, name; + aEntry->GetTitle(title); + aEntry->GetName(name); + + SHEntrySharedParentState* shared; + if (mozilla::SessionHistoryInParent()) { + shared = static_cast<SessionHistoryEntry*>(aEntry)->SharedInfo(); + } else { + shared = static_cast<nsSHEntry*>(aEntry)->GetState(); + } + + nsID docShellId; + aEntry->GetDocshellID(docShellId); + + int32_t childCount = aEntry->GetChildCount(); + + MOZ_LOG(gSHLog, LogLevel::Debug, + ("%s%s+- %i SH Entry %p %" PRIu64 " %s\n", aIsCurrent ? ">" : " ", + aPrefix.get(), aIndex, aEntry, shared->GetId(), + nsIDToCString(docShellId).get())); + + nsCString prefix(aPrefix); + if (aIndex < aTotal - 1) { + prefix.AppendLiteral("| "); + } else { + prefix.AppendLiteral(" "); + } + + MOZ_LOG(gSHLog, LogLevel::Debug, + (" %s%s URL = %s\n", prefix.get(), childCount > 0 ? "|" : " ", + uri->GetSpecOrDefault().get())); + MOZ_LOG(gSHLog, LogLevel::Debug, + (" %s%s Title = %s\n", prefix.get(), childCount > 0 ? "|" : " ", + NS_LossyConvertUTF16toASCII(title).get())); + MOZ_LOG(gSHLog, LogLevel::Debug, + (" %s%s Name = %s\n", prefix.get(), childCount > 0 ? "|" : " ", + NS_LossyConvertUTF16toASCII(name).get())); + MOZ_LOG( + gSHLog, LogLevel::Debug, + (" %s%s Is in BFCache = %s\n", prefix.get(), childCount > 0 ? "|" : " ", + aEntry->GetIsInBFCache() ? "true" : "false")); + + nsCOMPtr<nsISHEntry> prevChild; + for (int32_t i = 0; i < childCount; ++i) { + nsCOMPtr<nsISHEntry> child; + aEntry->GetChildAt(i, getter_AddRefs(child)); + LogEntry(child, i, childCount, prefix, false); + child.swap(prevChild); + } +} + +void nsSHistory::LogHistory() { + if (!MOZ_LOG_TEST(gSHLog, LogLevel::Debug)) { + return; + } + + MOZ_LOG(gSHLog, LogLevel::Debug, ("nsSHistory %p\n", this)); + int32_t length = Length(); + for (int32_t i = 0; i < length; i++) { + LogEntry(mEntries[i], i, length, EmptyCString(), i == mIndex); + } +} + +void nsSHistory::WindowIndices(int32_t aIndex, int32_t* aOutStartIndex, + int32_t* aOutEndIndex) { + *aOutStartIndex = std::max(0, aIndex - nsSHistory::VIEWER_WINDOW); + *aOutEndIndex = std::min(Length() - 1, aIndex + nsSHistory::VIEWER_WINDOW); +} + +static void MarkAsInitialEntry( + SessionHistoryEntry* aEntry, + nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable) { + if (!aEntry->BCHistoryLength().Modified()) { + ++(aEntry->BCHistoryLength()); + } + aHashtable.InsertOrUpdate(aEntry->DocshellID(), aEntry); + for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) { + if (entry) { + MarkAsInitialEntry(entry, aHashtable); + } + } +} + +static void ClearEntries(SessionHistoryEntry* aEntry) { + aEntry->ClearBCHistoryLength(); + for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) { + if (entry) { + ClearEntries(entry); + } + } +} + +NS_IMETHODIMP +nsSHistory::PurgeHistory(int32_t aNumEntries) { + if (Length() <= 0 || aNumEntries <= 0) { + return NS_ERROR_FAILURE; + } + + SHistoryChangeNotifier change(this); + + aNumEntries = std::min(aNumEntries, Length()); + + NotifyListeners(mListeners, + [aNumEntries](auto l) { l->OnHistoryPurge(aNumEntries); }); + + // Set all the entries hanging of the first entry that we keep + // (mEntries[aNumEntries]) as being created as the result of a load + // (so contributing one to their BCHistoryLength). + nsTHashMap<nsIDHashKey, SessionHistoryEntry*> docshellIDToEntry; + if (aNumEntries != Length()) { + nsCOMPtr<SessionHistoryEntry> she = + do_QueryInterface(mEntries[aNumEntries]); + if (she) { + MarkAsInitialEntry(she, docshellIDToEntry); + } + } + + // Reset the BCHistoryLength of all the entries that we're removing to a new + // counter with value 0 while decreasing their contribution to a shared + // BCHistoryLength. The end result is that they don't contribute to the + // BCHistoryLength of any other entry anymore. + for (int32_t i = 0; i < aNumEntries; ++i) { + nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(mEntries[i]); + if (she) { + ClearEntries(she); + } + } + + RefPtr<BrowsingContext> rootBC = GetBrowsingContext(); + if (rootBC) { + rootBC->PreOrderWalk([&docshellIDToEntry](BrowsingContext* aBC) { + SessionHistoryEntry* entry = docshellIDToEntry.Get(aBC->GetHistoryID()); + Unused << aBC->SetHistoryEntryCount( + entry ? uint32_t(entry->BCHistoryLength()) : 0); + }); + } + + // Remove the first `aNumEntries` entries. + mEntries.RemoveElementsAt(0, aNumEntries); + + // Adjust the indices, but don't let them go below -1. + mIndex -= aNumEntries; + mIndex = std::max(mIndex, -1); + mRequestedIndex -= aNumEntries; + mRequestedIndex = std::max(mRequestedIndex, -1); + + if (rootBC && rootBC->GetDocShell()) { + rootBC->GetDocShell()->HistoryPurged(aNumEntries); + } + + UpdateRootBrowsingContextState(rootBC); + + return NS_OK; +} + +NS_IMETHODIMP +nsSHistory::AddSHistoryListener(nsISHistoryListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + + // Check if the listener supports Weak Reference. This is a must. + // This listener functionality is used by embedders and we want to + // have the right ownership with who ever listens to SHistory + nsWeakPtr listener = do_GetWeakReference(aListener); + if (!listener) { + return NS_ERROR_FAILURE; + } + + mListeners.AppendElementUnlessExists(listener); + return NS_OK; +} + +void nsSHistory::NotifyListenersContentViewerEvicted(uint32_t aNumEvicted) { + NotifyListeners(mListeners, [aNumEvicted](auto l) { + l->OnContentViewerEvicted(aNumEvicted); + }); +} + +NS_IMETHODIMP +nsSHistory::RemoveSHistoryListener(nsISHistoryListener* aListener) { + // Make sure the listener that wants to be removed is the + // one we have in store. + nsWeakPtr listener = do_GetWeakReference(aListener); + mListeners.RemoveElement(listener); + return NS_OK; +} + +/* Replace an entry in the History list at a particular index. + * Do not update index or count. + */ +NS_IMETHODIMP +nsSHistory::ReplaceEntry(int32_t aIndex, nsISHEntry* aReplaceEntry) { + NS_ENSURE_ARG(aReplaceEntry); + + if (aIndex < 0 || aIndex >= Length()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISHistory> shistoryOfEntry = aReplaceEntry->GetShistory(); + if (shistoryOfEntry && shistoryOfEntry != this) { + NS_WARNING( + "The entry has been associated to another nsISHistory instance. " + "Try nsISHEntry.clone() and nsISHEntry.abandonBFCacheEntry() " + "first if you're copying an entry from another nsISHistory."); + return NS_ERROR_FAILURE; + } + + aReplaceEntry->SetShistory(this); + + NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); }); + + aReplaceEntry->SetPersist(true); + mEntries[aIndex] = aReplaceEntry; + + UpdateRootBrowsingContextState(); + + return NS_OK; +} + +// Calls OnHistoryReload on all registered session history listeners. +// Listeners may return 'false' to cancel an action so make sure that we +// set the return value to 'false' if one of the listeners wants to cancel. +NS_IMETHODIMP +nsSHistory::NotifyOnHistoryReload(bool* aCanReload) { + *aCanReload = true; + + for (const nsWeakPtr& weakPtr : mListeners.EndLimitedRange()) { + nsCOMPtr<nsISHistoryListener> listener = do_QueryReferent(weakPtr); + if (listener) { + bool retval = true; + + if (NS_SUCCEEDED(listener->OnHistoryReload(&retval)) && !retval) { + *aCanReload = false; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsSHistory::EvictOutOfRangeContentViewers(int32_t aIndex) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, + ("nsSHistory::EvictOutOfRangeContentViewers %i", aIndex)); + + // Check our per SHistory object limit in the currently navigated SHistory + EvictOutOfRangeWindowContentViewers(aIndex); + // Check our total limit across all SHistory objects + GloballyEvictContentViewers(); + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHistory::EvictContentViewersOrReplaceEntry(nsISHEntry* aNewSHEntry, + bool aReplace) { + if (!aReplace) { + int32_t curIndex; + GetIndex(&curIndex); + if (curIndex > -1) { + EvictOutOfRangeContentViewers(curIndex); + } + } else { + nsCOMPtr<nsISHEntry> rootSHEntry = nsSHistory::GetRootSHEntry(aNewSHEntry); + + int32_t index = GetIndexOfEntry(rootSHEntry); + if (index > -1) { + ReplaceEntry(index, rootSHEntry); + } + } +} + +NS_IMETHODIMP +nsSHistory::EvictAllContentViewers() { + // XXXbz we don't actually do a good job of evicting things as we should, so + // we might have viewers quite far from mIndex. So just evict everything. + for (int32_t i = 0; i < Length(); i++) { + EvictContentViewerForEntry(mEntries[i]); + } + + return NS_OK; +} + +static void FinishRestore(CanonicalBrowsingContext* aBrowsingContext, + nsDocShellLoadState* aLoadState, + SessionHistoryEntry* aEntry, + nsFrameLoader* aFrameLoader, bool aCanSave) { + MOZ_ASSERT(aEntry); + MOZ_ASSERT(aFrameLoader); + + aEntry->SetFrameLoader(nullptr); + + nsCOMPtr<nsISHistory> shistory = aEntry->GetShistory(); + int32_t indexOfHistoryLoad = + shistory ? shistory->GetIndexOfEntry(aEntry) : -1; + + nsCOMPtr<nsFrameLoaderOwner> frameLoaderOwner = + do_QueryInterface(aBrowsingContext->GetEmbedderElement()); + if (frameLoaderOwner && aFrameLoader->GetMaybePendingBrowsingContext() && + indexOfHistoryLoad >= 0) { + RefPtr<BrowsingContextWebProgress> webProgress = + aBrowsingContext->GetWebProgress(); + if (webProgress) { + // Synthesize a STATE_START WebProgress state change event from here + // in order to ensure emitting it on the BrowsingContext we navigate + // *from* instead of the BrowsingContext we navigate *to*. This will fire + // before and the next one will be ignored by BrowsingContextWebProgress: + // https://searchfox.org/mozilla-central/rev/77f0b36028b2368e342c982ea47609040b399d89/docshell/base/BrowsingContextWebProgress.cpp#196-203 + nsCOMPtr<nsIURI> nextURI = aEntry->GetURI(); + nsCOMPtr<nsIURI> nextOriginalURI = aEntry->GetOriginalURI(); + nsCOMPtr<nsIRequest> request = MakeAndAddRef<RemoteWebProgressRequest>( + nextURI, nextOriginalURI ? nextOriginalURI : nextURI, + ""_ns /* aMatchedList */); + webProgress->OnStateChange(webProgress, request, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_DOCUMENT | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_WINDOW | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); + } + + RefPtr<CanonicalBrowsingContext> loadingBC = + aFrameLoader->GetMaybePendingBrowsingContext()->Canonical(); + RefPtr<nsFrameLoader> currentFrameLoader = + frameLoaderOwner->GetFrameLoader(); + // The current page can be bfcached, store the + // nsFrameLoader in the current SessionHistoryEntry. + RefPtr<SessionHistoryEntry> currentSHEntry = + aBrowsingContext->GetActiveSessionHistoryEntry(); + if (currentSHEntry) { + // Update layout history state now, before we change the IsInBFCache flag + // and the active session history entry. + aBrowsingContext->SynchronizeLayoutHistoryState(); + + if (aCanSave) { + currentSHEntry->SetFrameLoader(currentFrameLoader); + Unused << aBrowsingContext->SetIsInBFCache(true); + } + } + + if (aBrowsingContext->IsActive()) { + loadingBC->PreOrderWalk([&](BrowsingContext* aContext) { + if (BrowserParent* bp = aContext->Canonical()->GetBrowserParent()) { + ProcessPriorityManager::BrowserPriorityChanged(bp, true); + } + }); + } + + if (aEntry) { + aEntry->SetWireframe(Nothing()); + } + + // ReplacedBy will swap the entry back. + aBrowsingContext->SetActiveSessionHistoryEntry(aEntry); + loadingBC->SetActiveSessionHistoryEntry(nullptr); + NavigationIsolationOptions options; + aBrowsingContext->ReplacedBy(loadingBC, options); + + // Assuming we still have the session history, update the index. + if (loadingBC->GetSessionHistory()) { + shistory->InternalSetRequestedIndex(indexOfHistoryLoad); + shistory->UpdateIndex(); + } + loadingBC->HistoryCommitIndexAndLength(); + + // ResetSHEntryHasUserInteractionCache(); ? + // browser.navigation.requireUserInteraction is still + // disabled everywhere. + + frameLoaderOwner->RestoreFrameLoaderFromBFCache(aFrameLoader); + // EvictOutOfRangeContentViewers is called here explicitly to + // possibly evict the now in the bfcache document. + // HistoryCommitIndexAndLength might not have evicted that before the + // FrameLoader swap. + shistory->EvictOutOfRangeContentViewers(indexOfHistoryLoad); + + // The old page can't be stored in the bfcache, + // destroy the nsFrameLoader. + if (!aCanSave && currentFrameLoader) { + currentFrameLoader->Destroy(); + } + + Unused << loadingBC->SetIsInBFCache(false); + + // We need to call this after we've restored the page from BFCache (see + // SetIsInBFCache(false) above), so that the page is not frozen anymore and + // the right focus events are fired. + frameLoaderOwner->UpdateFocusAndMouseEnterStateAfterFrameLoaderChange(); + + return; + } + + aFrameLoader->Destroy(); + + // Fall back to do a normal load. + aBrowsingContext->LoadURI(aLoadState, false); +} + +/* static */ +void nsSHistory::LoadURIOrBFCache(LoadEntryResult& aLoadEntry) { + if (mozilla::BFCacheInParent() && aLoadEntry.mBrowsingContext->IsTop()) { + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr<nsDocShellLoadState> loadState = aLoadEntry.mLoadState; + RefPtr<CanonicalBrowsingContext> canonicalBC = + aLoadEntry.mBrowsingContext->Canonical(); + nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(loadState->SHEntry()); + nsCOMPtr<SessionHistoryEntry> currentShe = + canonicalBC->GetActiveSessionHistoryEntry(); + MOZ_ASSERT(she); + RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader(); + if (frameLoader && + (!currentShe || (she->SharedInfo() != currentShe->SharedInfo() && + !currentShe->GetFrameLoader()))) { + bool canSave = (!currentShe || currentShe->GetSaveLayoutStateFlag()) && + canonicalBC->AllowedInBFCache(Nothing(), nullptr); + + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, + ("nsSHistory::LoadURIOrBFCache " + "saving presentation=%i", + canSave)); + + nsCOMPtr<nsFrameLoaderOwner> frameLoaderOwner = + do_QueryInterface(canonicalBC->GetEmbedderElement()); + if (frameLoaderOwner) { + RefPtr<nsFrameLoader> currentFrameLoader = + frameLoaderOwner->GetFrameLoader(); + if (currentFrameLoader && + currentFrameLoader->GetMaybePendingBrowsingContext()) { + if (WindowGlobalParent* wgp = + currentFrameLoader->GetMaybePendingBrowsingContext() + ->Canonical() + ->GetCurrentWindowGlobal()) { + wgp->PermitUnload([canonicalBC, loadState, she, frameLoader, + currentFrameLoader, canSave](bool aAllow) { + if (aAllow && !canonicalBC->IsReplaced()) { + FinishRestore(canonicalBC, loadState, she, frameLoader, + canSave && canonicalBC->AllowedInBFCache( + Nothing(), nullptr)); + } else if (currentFrameLoader->GetMaybePendingBrowsingContext()) { + nsISHistory* shistory = + currentFrameLoader->GetMaybePendingBrowsingContext() + ->Canonical() + ->GetSessionHistory(); + if (shistory) { + shistory->InternalSetRequestedIndex(-1); + } + } + }); + return; + } + } + } + + FinishRestore(canonicalBC, loadState, she, frameLoader, canSave); + return; + } + if (frameLoader) { + she->SetFrameLoader(nullptr); + frameLoader->Destroy(); + } + } + + RefPtr<BrowsingContext> bc = aLoadEntry.mBrowsingContext; + RefPtr<nsDocShellLoadState> loadState = aLoadEntry.mLoadState; + bc->LoadURI(loadState, false); +} + +/* static */ +void nsSHistory::LoadURIs(nsTArray<LoadEntryResult>& aLoadResults) { + for (LoadEntryResult& loadEntry : aLoadResults) { + LoadURIOrBFCache(loadEntry); + } +} + +NS_IMETHODIMP +nsSHistory::Reload(uint32_t aReloadFlags) { + nsTArray<LoadEntryResult> loadResults; + nsresult rv = Reload(aReloadFlags, loadResults); + NS_ENSURE_SUCCESS(rv, rv); + + if (loadResults.IsEmpty()) { + return NS_OK; + } + + LoadURIs(loadResults); + return NS_OK; +} + +nsresult nsSHistory::Reload(uint32_t aReloadFlags, + nsTArray<LoadEntryResult>& aLoadResults) { + MOZ_ASSERT(aLoadResults.IsEmpty()); + + uint32_t loadType; + if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY && + aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE) { + loadType = LOAD_RELOAD_BYPASS_PROXY_AND_CACHE; + } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY) { + loadType = LOAD_RELOAD_BYPASS_PROXY; + } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE) { + loadType = LOAD_RELOAD_BYPASS_CACHE; + } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE) { + loadType = LOAD_RELOAD_CHARSET_CHANGE; + } else { + loadType = LOAD_RELOAD_NORMAL; + } + + // We are reloading. Send Reload notifications. + // nsDocShellLoadFlagType is not public, where as nsIWebNavigation + // is public. So send the reload notifications with the + // nsIWebNavigation flags. + bool canNavigate = true; + MOZ_ALWAYS_SUCCEEDS(NotifyOnHistoryReload(&canNavigate)); + if (!canNavigate) { + return NS_OK; + } + + nsresult rv = LoadEntry( + mIndex, loadType, HIST_CMD_RELOAD, aLoadResults, /* aSameEpoch */ false, + /* aLoadCurrentEntry */ true, + aReloadFlags & nsIWebNavigation::LOAD_FLAGS_USER_ACTIVATION); + if (NS_FAILED(rv)) { + aLoadResults.Clear(); + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsSHistory::ReloadCurrentEntry() { + nsTArray<LoadEntryResult> loadResults; + nsresult rv = ReloadCurrentEntry(loadResults); + NS_ENSURE_SUCCESS(rv, rv); + + LoadURIs(loadResults); + return NS_OK; +} + +nsresult nsSHistory::ReloadCurrentEntry( + nsTArray<LoadEntryResult>& aLoadResults) { + // Notify listeners + NotifyListeners(mListeners, [](auto l) { l->OnHistoryGotoIndex(); }); + + return LoadEntry(mIndex, LOAD_HISTORY, HIST_CMD_RELOAD, aLoadResults, + /* aSameEpoch */ false, /* aLoadCurrentEntry */ true, + /* aUserActivation */ false); +} + +void nsSHistory::EvictOutOfRangeWindowContentViewers(int32_t aIndex) { + // XXX rename method to EvictContentViewersExceptAroundIndex, or something. + + // We need to release all content viewers that are no longer in the range + // + // aIndex - VIEWER_WINDOW to aIndex + VIEWER_WINDOW + // + // to ensure that this SHistory object isn't responsible for more than + // VIEWER_WINDOW content viewers. But our job is complicated by the + // fact that two entries which are related by either hash navigations or + // history.pushState will have the same content viewer. + // + // To illustrate the issue, suppose VIEWER_WINDOW = 3 and we have four + // linked entries in our history. Suppose we then add a new content + // viewer and call into this function. So the history looks like: + // + // A A A A B + // + * + // + // where the letters are content viewers and + and * denote the beginning and + // end of the range aIndex +/- VIEWER_WINDOW. + // + // Although one copy of the content viewer A exists outside the range, we + // don't want to evict A, because it has other copies in range! + // + // We therefore adjust our eviction strategy to read: + // + // Evict each content viewer outside the range aIndex -/+ + // VIEWER_WINDOW, unless that content viewer also appears within the + // range. + // + // (Note that it's entirely legal to have two copies of one content viewer + // separated by a different content viewer -- call pushState twice, go back + // once, and refresh -- so we can't rely on identical viewers only appearing + // adjacent to one another.) + + if (aIndex < 0) { + return; + } + NS_ENSURE_TRUE_VOID(aIndex < Length()); + + // Calculate the range that's safe from eviction. + int32_t startSafeIndex, endSafeIndex; + WindowIndices(aIndex, &startSafeIndex, &endSafeIndex); + + LOG( + ("EvictOutOfRangeWindowContentViewers(index=%d), " + "Length()=%d. Safe range [%d, %d]", + aIndex, Length(), startSafeIndex, endSafeIndex)); + + // The content viewers in range aIndex -/+ VIEWER_WINDOW will not be + // evicted. Collect a set of them so we don't accidentally evict one of them + // if it appears outside this range. + nsCOMArray<nsIContentViewer> safeViewers; + nsTArray<RefPtr<nsFrameLoader>> safeFrameLoaders; + for (int32_t i = startSafeIndex; i <= endSafeIndex; i++) { + nsCOMPtr<nsIContentViewer> viewer = mEntries[i]->GetContentViewer(); + if (viewer) { + safeViewers.AppendObject(viewer); + } else if (nsCOMPtr<SessionHistoryEntry> she = + do_QueryInterface(mEntries[i])) { + nsFrameLoader* frameLoader = she->GetFrameLoader(); + if (frameLoader) { + safeFrameLoaders.AppendElement(frameLoader); + } + } + } + + // Walk the SHistory list and evict any content viewers that aren't safe. + // (It's important that the condition checks Length(), rather than a cached + // copy of Length(), because the length might change between iterations.) + for (int32_t i = 0; i < Length(); i++) { + nsCOMPtr<nsISHEntry> entry = mEntries[i]; + nsCOMPtr<nsIContentViewer> viewer = entry->GetContentViewer(); + if (viewer) { + if (safeViewers.IndexOf(viewer) == -1) { + EvictContentViewerForEntry(entry); + } + } else if (nsCOMPtr<SessionHistoryEntry> she = + do_QueryInterface(mEntries[i])) { + nsFrameLoader* frameLoader = she->GetFrameLoader(); + if (frameLoader) { + if (!safeFrameLoaders.Contains(frameLoader)) { + EvictContentViewerForEntry(entry); + } + } + } + } +} + +namespace { + +class EntryAndDistance { + public: + EntryAndDistance(nsSHistory* aSHistory, nsISHEntry* aEntry, uint32_t aDist) + : mSHistory(aSHistory), + mEntry(aEntry), + mViewer(aEntry->GetContentViewer()), + mLastTouched(mEntry->GetLastTouched()), + mDistance(aDist) { + nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aEntry); + if (she) { + mFrameLoader = she->GetFrameLoader(); + } + NS_ASSERTION(mViewer || mFrameLoader, + "Entry should have a content viewer or frame loader."); + } + + bool operator<(const EntryAndDistance& aOther) const { + // Compare distances first, and fall back to last-accessed times. + if (aOther.mDistance != this->mDistance) { + return this->mDistance < aOther.mDistance; + } + + return this->mLastTouched < aOther.mLastTouched; + } + + bool operator==(const EntryAndDistance& aOther) const { + // This is a little silly; we need == so the default comaprator can be + // instantiated, but this function is never actually called when we sort + // the list of EntryAndDistance objects. + return aOther.mDistance == this->mDistance && + aOther.mLastTouched == this->mLastTouched; + } + + RefPtr<nsSHistory> mSHistory; + nsCOMPtr<nsISHEntry> mEntry; + nsCOMPtr<nsIContentViewer> mViewer; + RefPtr<nsFrameLoader> mFrameLoader; + uint32_t mLastTouched; + int32_t mDistance; +}; + +} // namespace + +// static +void nsSHistory::GloballyEvictContentViewers() { + // First, collect from each SHistory object the entries which have a cached + // content viewer. Associate with each entry its distance from its SHistory's + // current index. + + nsTArray<EntryAndDistance> entries; + + for (auto shist : gSHistoryList.mList) { + // Maintain a list of the entries which have viewers and belong to + // this particular shist object. We'll add this list to the global list, + // |entries|, eventually. + nsTArray<EntryAndDistance> shEntries; + + // Content viewers are likely to exist only within shist->mIndex -/+ + // VIEWER_WINDOW, so only search within that range. + // + // A content viewer might exist outside that range due to either: + // + // * history.pushState or hash navigations, in which case a copy of the + // content viewer should exist within the range, or + // + // * bugs which cause us not to call nsSHistory::EvictContentViewers() + // often enough. Once we do call EvictContentViewers() for the + // SHistory object in question, we'll do a full search of its history + // and evict the out-of-range content viewers, so we don't bother here. + // + int32_t startIndex, endIndex; + shist->WindowIndices(shist->mIndex, &startIndex, &endIndex); + for (int32_t i = startIndex; i <= endIndex; i++) { + nsCOMPtr<nsISHEntry> entry = shist->mEntries[i]; + nsCOMPtr<nsIContentViewer> contentViewer = entry->GetContentViewer(); + + bool found = false; + bool hasContentViewerOrFrameLoader = false; + if (contentViewer) { + hasContentViewerOrFrameLoader = true; + // Because one content viewer might belong to multiple SHEntries, we + // have to search through shEntries to see if we already know + // about this content viewer. If we find the viewer, update its + // distance from the SHistory's index and continue. + for (uint32_t j = 0; j < shEntries.Length(); j++) { + EntryAndDistance& container = shEntries[j]; + if (container.mViewer == contentViewer) { + container.mDistance = + std::min(container.mDistance, DeprecatedAbs(i - shist->mIndex)); + found = true; + break; + } + } + } else if (nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(entry)) { + if (RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader()) { + hasContentViewerOrFrameLoader = true; + // Similar search as above but using frameloader. + for (uint32_t j = 0; j < shEntries.Length(); j++) { + EntryAndDistance& container = shEntries[j]; + if (container.mFrameLoader == frameLoader) { + container.mDistance = std::min(container.mDistance, + DeprecatedAbs(i - shist->mIndex)); + found = true; + break; + } + } + } + } + + // If we didn't find a EntryAndDistance for this content viewer / + // frameloader, make a new one. + if (hasContentViewerOrFrameLoader && !found) { + EntryAndDistance container(shist, entry, + DeprecatedAbs(i - shist->mIndex)); + shEntries.AppendElement(container); + } + } + + // We've found all the entries belonging to shist which have viewers. + // Add those entries to our global list and move on. + entries.AppendElements(shEntries); + } + + // We now have collected all cached content viewers. First check that we + // have enough that we actually need to evict some. + if ((int32_t)entries.Length() <= sHistoryMaxTotalViewers) { + return; + } + + // If we need to evict, sort our list of entries and evict the largest + // ones. (We could of course get better algorithmic complexity here by using + // a heap or something more clever. But sHistoryMaxTotalViewers isn't large, + // so let's not worry about it.) + entries.Sort(); + + for (int32_t i = entries.Length() - 1; i >= sHistoryMaxTotalViewers; --i) { + (entries[i].mSHistory)->EvictContentViewerForEntry(entries[i].mEntry); + } +} + +nsresult nsSHistory::FindEntryForBFCache(SHEntrySharedParentState* aEntry, + nsISHEntry** aResult, + int32_t* aResultIndex) { + *aResult = nullptr; + *aResultIndex = -1; + + int32_t startIndex, endIndex; + WindowIndices(mIndex, &startIndex, &endIndex); + + for (int32_t i = startIndex; i <= endIndex; ++i) { + nsCOMPtr<nsISHEntry> shEntry = mEntries[i]; + + // Does shEntry have the same BFCacheEntry as the argument to this method? + if (shEntry->HasBFCacheEntry(aEntry)) { + shEntry.forget(aResult); + *aResultIndex = i; + return NS_OK; + } + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP_(void) +nsSHistory::EvictExpiredContentViewerForEntry( + SHEntrySharedParentState* aEntry) { + int32_t index; + nsCOMPtr<nsISHEntry> shEntry; + FindEntryForBFCache(aEntry, getter_AddRefs(shEntry), &index); + + if (index == mIndex) { + NS_WARNING("How did the current SHEntry expire?"); + } + + if (shEntry) { + EvictContentViewerForEntry(shEntry); + } +} + +NS_IMETHODIMP_(void) +nsSHistory::AddToExpirationTracker(SHEntrySharedParentState* aEntry) { + RefPtr<SHEntrySharedParentState> entry = aEntry; + if (!mHistoryTracker || !entry) { + return; + } + + mHistoryTracker->AddObject(entry); + return; +} + +NS_IMETHODIMP_(void) +nsSHistory::RemoveFromExpirationTracker(SHEntrySharedParentState* aEntry) { + RefPtr<SHEntrySharedParentState> entry = aEntry; + MOZ_ASSERT(mHistoryTracker && !mHistoryTracker->IsEmpty()); + if (!mHistoryTracker || !entry) { + return; + } + + mHistoryTracker->RemoveObject(entry); +} + +// Evicts all content viewers in all history objects. This is very +// inefficient, because it requires a linear search through all SHistory +// objects for each viewer to be evicted. However, this method is called +// infrequently -- only when the disk or memory cache is cleared. + +// static +void nsSHistory::GloballyEvictAllContentViewers() { + int32_t maxViewers = sHistoryMaxTotalViewers; + sHistoryMaxTotalViewers = 0; + GloballyEvictContentViewers(); + sHistoryMaxTotalViewers = maxViewers; +} + +void GetDynamicChildren(nsISHEntry* aEntry, nsTArray<nsID>& aDocshellIDs) { + int32_t count = aEntry->GetChildCount(); + for (int32_t i = 0; i < count; ++i) { + nsCOMPtr<nsISHEntry> child; + aEntry->GetChildAt(i, getter_AddRefs(child)); + if (child) { + if (child->IsDynamicallyAdded()) { + child->GetDocshellID(*aDocshellIDs.AppendElement()); + } else { + GetDynamicChildren(child, aDocshellIDs); + } + } + } +} + +bool RemoveFromSessionHistoryEntry(nsISHEntry* aRoot, + nsTArray<nsID>& aDocshellIDs) { + bool didRemove = false; + int32_t childCount = aRoot->GetChildCount(); + for (int32_t i = childCount - 1; i >= 0; --i) { + nsCOMPtr<nsISHEntry> child; + aRoot->GetChildAt(i, getter_AddRefs(child)); + if (child) { + nsID docshelldID; + child->GetDocshellID(docshelldID); + if (aDocshellIDs.Contains(docshelldID)) { + didRemove = true; + aRoot->RemoveChild(child); + } else if (RemoveFromSessionHistoryEntry(child, aDocshellIDs)) { + didRemove = true; + } + } + } + return didRemove; +} + +bool RemoveChildEntries(nsISHistory* aHistory, int32_t aIndex, + nsTArray<nsID>& aEntryIDs) { + nsCOMPtr<nsISHEntry> root; + aHistory->GetEntryAtIndex(aIndex, getter_AddRefs(root)); + return root ? RemoveFromSessionHistoryEntry(root, aEntryIDs) : false; +} + +bool IsSameTree(nsISHEntry* aEntry1, nsISHEntry* aEntry2) { + if (!aEntry1 && !aEntry2) { + return true; + } + if ((!aEntry1 && aEntry2) || (aEntry1 && !aEntry2)) { + return false; + } + uint32_t id1 = aEntry1->GetID(); + uint32_t id2 = aEntry2->GetID(); + if (id1 != id2) { + return false; + } + + int32_t count1 = aEntry1->GetChildCount(); + int32_t count2 = aEntry2->GetChildCount(); + // We allow null entries in the end of the child list. + int32_t count = std::max(count1, count2); + for (int32_t i = 0; i < count; ++i) { + nsCOMPtr<nsISHEntry> child1, child2; + aEntry1->GetChildAt(i, getter_AddRefs(child1)); + aEntry2->GetChildAt(i, getter_AddRefs(child2)); + if (!IsSameTree(child1, child2)) { + return false; + } + } + + return true; +} + +bool nsSHistory::RemoveDuplicate(int32_t aIndex, bool aKeepNext) { + NS_ASSERTION(aIndex >= 0, "aIndex must be >= 0!"); + NS_ASSERTION(aIndex != 0 || aKeepNext, + "If we're removing index 0 we must be keeping the next"); + NS_ASSERTION(aIndex != mIndex, "Shouldn't remove mIndex!"); + + int32_t compareIndex = aKeepNext ? aIndex + 1 : aIndex - 1; + + nsresult rv; + nsCOMPtr<nsISHEntry> root1, root2; + rv = GetEntryAtIndex(aIndex, getter_AddRefs(root1)); + if (NS_FAILED(rv)) { + return false; + } + rv = GetEntryAtIndex(compareIndex, getter_AddRefs(root2)); + if (NS_FAILED(rv)) { + return false; + } + + SHistoryChangeNotifier change(this); + + if (IsSameTree(root1, root2)) { + if (aIndex < compareIndex) { + // If we're removing the entry with the lower index we need to move its + // BCHistoryLength to the entry we're keeping. If we're removing the entry + // with the higher index then it shouldn't have a modified + // BCHistoryLength. + UpdateEntryLength(root1, root2, true); + } + nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(root1); + if (she) { + ClearEntries(she); + } + mEntries.RemoveElementAt(aIndex); + + // FIXME Bug 1546350: Reimplement history listeners. + // if (mRootBC && mRootBC->GetDocShell()) { + // static_cast<nsDocShell*>(mRootBC->GetDocShell()) + // ->HistoryEntryRemoved(aIndex); + //} + + // Adjust our indices to reflect the removed entry. + if (mIndex > aIndex) { + mIndex = mIndex - 1; + } + + // NB: If the entry we are removing is the entry currently + // being navigated to (mRequestedIndex) then we adjust the index + // only if we're not keeping the next entry (because if we are keeping + // the next entry (because the current is a duplicate of the next), then + // that entry slides into the spot that we're currently pointing to. + // We don't do this adjustment for mIndex because mIndex cannot equal + // aIndex. + + // NB: We don't need to guard on mRequestedIndex being nonzero here, + // because either they're strictly greater than aIndex which is at least + // zero, or they are equal to aIndex in which case aKeepNext must be true + // if aIndex is zero. + if (mRequestedIndex > aIndex || (mRequestedIndex == aIndex && !aKeepNext)) { + mRequestedIndex = mRequestedIndex - 1; + } + + return true; + } + return false; +} + +NS_IMETHODIMP_(void) +nsSHistory::RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex) { + bool didRemove; + RemoveEntries(aIDs, aStartIndex, &didRemove); + if (didRemove) { + RefPtr<BrowsingContext> rootBC = GetBrowsingContext(); + if (rootBC && rootBC->GetDocShell()) { + rootBC->GetDocShell()->DispatchLocationChangeEvent(); + } + } +} + +void nsSHistory::RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex, + bool* aDidRemove) { + SHistoryChangeNotifier change(this); + + int32_t index = aStartIndex; + while (index >= 0 && RemoveChildEntries(this, --index, aIDs)) { + } + int32_t minIndex = index; + index = aStartIndex; + while (index >= 0 && RemoveChildEntries(this, index++, aIDs)) { + } + + // We need to remove duplicate nsSHEntry trees. + *aDidRemove = false; + while (index > minIndex) { + if (index != mIndex && RemoveDuplicate(index, index < mIndex)) { + *aDidRemove = true; + } + --index; + } + + UpdateRootBrowsingContextState(); +} + +void nsSHistory::RemoveFrameEntries(nsISHEntry* aEntry) { + int32_t count = aEntry->GetChildCount(); + AutoTArray<nsID, 16> ids; + for (int32_t i = 0; i < count; ++i) { + nsCOMPtr<nsISHEntry> child; + aEntry->GetChildAt(i, getter_AddRefs(child)); + if (child) { + child->GetDocshellID(*ids.AppendElement()); + } + } + RemoveEntries(ids, mIndex); +} + +void nsSHistory::RemoveDynEntries(int32_t aIndex, nsISHEntry* aEntry) { + // Remove dynamic entries which are at the index and belongs to the container. + nsCOMPtr<nsISHEntry> entry(aEntry); + if (!entry) { + GetEntryAtIndex(aIndex, getter_AddRefs(entry)); + } + + if (entry) { + AutoTArray<nsID, 16> toBeRemovedEntries; + GetDynamicChildren(entry, toBeRemovedEntries); + if (toBeRemovedEntries.Length()) { + RemoveEntries(toBeRemovedEntries, aIndex); + } + } +} + +void nsSHistory::RemoveDynEntriesForBFCacheEntry(nsIBFCacheEntry* aBFEntry) { + int32_t index; + nsCOMPtr<nsISHEntry> shEntry; + FindEntryForBFCache(static_cast<nsSHEntryShared*>(aBFEntry), + getter_AddRefs(shEntry), &index); + if (shEntry) { + RemoveDynEntries(index, shEntry); + } +} + +NS_IMETHODIMP +nsSHistory::UpdateIndex() { + SHistoryChangeNotifier change(this); + + // Update the actual index with the right value. + if (mIndex != mRequestedIndex && mRequestedIndex != -1) { + mIndex = mRequestedIndex; + } + + mRequestedIndex = -1; + return NS_OK; +} + +NS_IMETHODIMP +nsSHistory::GotoIndex(int32_t aIndex, bool aUserActivation) { + nsTArray<LoadEntryResult> loadResults; + nsresult rv = GotoIndex(aIndex, loadResults, /*aSameEpoch*/ false, + aIndex == mIndex, aUserActivation); + NS_ENSURE_SUCCESS(rv, rv); + + LoadURIs(loadResults); + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHistory::EnsureCorrectEntryAtCurrIndex(nsISHEntry* aEntry) { + int index = mRequestedIndex == -1 ? mIndex : mRequestedIndex; + if (index > -1 && (mEntries[index] != aEntry)) { + ReplaceEntry(index, aEntry); + } +} + +nsresult nsSHistory::GotoIndex(int32_t aIndex, + nsTArray<LoadEntryResult>& aLoadResults, + bool aSameEpoch, bool aLoadCurrentEntry, + bool aUserActivation) { + return LoadEntry(aIndex, LOAD_HISTORY, HIST_CMD_GOTOINDEX, aLoadResults, + aSameEpoch, aLoadCurrentEntry, aUserActivation); +} + +NS_IMETHODIMP_(bool) +nsSHistory::HasUserInteractionAtIndex(int32_t aIndex) { + nsCOMPtr<nsISHEntry> entry; + GetEntryAtIndex(aIndex, getter_AddRefs(entry)); + if (!entry) { + return false; + } + return entry->GetHasUserInteraction(); +} + +nsresult nsSHistory::LoadNextPossibleEntry( + int32_t aNewIndex, long aLoadType, uint32_t aHistCmd, + nsTArray<LoadEntryResult>& aLoadResults, bool aLoadCurrentEntry, + bool aUserActivation) { + mRequestedIndex = -1; + if (aNewIndex < mIndex) { + return LoadEntry(aNewIndex - 1, aLoadType, aHistCmd, aLoadResults, + /*aSameEpoch*/ false, aLoadCurrentEntry, aUserActivation); + } + if (aNewIndex > mIndex) { + return LoadEntry(aNewIndex + 1, aLoadType, aHistCmd, aLoadResults, + /*aSameEpoch*/ false, aLoadCurrentEntry, aUserActivation); + } + return NS_ERROR_FAILURE; +} + +nsresult nsSHistory::LoadEntry(int32_t aIndex, long aLoadType, + uint32_t aHistCmd, + nsTArray<LoadEntryResult>& aLoadResults, + bool aSameEpoch, bool aLoadCurrentEntry, + bool aUserActivation) { + MOZ_LOG(gSHistoryLog, LogLevel::Debug, + ("LoadEntry(%d, 0x%lx, %u)", aIndex, aLoadType, aHistCmd)); + RefPtr<BrowsingContext> rootBC = GetBrowsingContext(); + if (!rootBC) { + return NS_ERROR_FAILURE; + } + + if (aIndex < 0 || aIndex >= Length()) { + MOZ_LOG(gSHistoryLog, LogLevel::Debug, ("Index out of range")); + // The index is out of range. + // Clear the requested index in case it had bogus value. This way the next + // load succeeds if the offset is reasonable. + mRequestedIndex = -1; + + return NS_ERROR_FAILURE; + } + + int32_t originalRequestedIndex = mRequestedIndex; + int32_t previousRequest = mRequestedIndex > -1 ? mRequestedIndex : mIndex; + int32_t requestedOffset = aIndex - previousRequest; + + // This is a normal local history navigation. + + nsCOMPtr<nsISHEntry> prevEntry; + nsCOMPtr<nsISHEntry> nextEntry; + GetEntryAtIndex(mIndex, getter_AddRefs(prevEntry)); + GetEntryAtIndex(aIndex, getter_AddRefs(nextEntry)); + if (!nextEntry || !prevEntry) { + mRequestedIndex = -1; + return NS_ERROR_FAILURE; + } + + if (mozilla::SessionHistoryInParent()) { + if (aHistCmd == HIST_CMD_GOTOINDEX) { + // https://html.spec.whatwg.org/#history-traversal: + // To traverse the history + // "If entry has a different Document object than the current entry, then + // run the following substeps: Remove any tasks queued by the history + // traversal task source..." + // + // aSameEpoch is true only if the navigations would have been + // generated in the same spin of the event loop (i.e. history.go(-2); + // history.go(-1)) and from the same content process. + if (aSameEpoch) { + bool same_doc = false; + prevEntry->SharesDocumentWith(nextEntry, &same_doc); + if (!same_doc) { + MOZ_LOG( + gSHistoryLog, LogLevel::Debug, + ("Aborting GotoIndex %d - same epoch and not same doc", aIndex)); + // Ignore this load. Before SessionHistoryInParent, this would + // have been dropped in InternalLoad after we filter out SameDoc + // loads. + return NS_ERROR_FAILURE; + } + } + } + } + // Keep note of requested history index in mRequestedIndex; after all bailouts + mRequestedIndex = aIndex; + + // Remember that this entry is getting loaded at this point in the sequence + + nextEntry->SetLastTouched(++gTouchCounter); + + // Get the uri for the entry we are about to visit + nsCOMPtr<nsIURI> nextURI = nextEntry->GetURI(); + + MOZ_ASSERT(nextURI, "nextURI can't be null"); + + // Send appropriate listener notifications. + if (aHistCmd == HIST_CMD_GOTOINDEX) { + // We are going somewhere else. This is not reload either + NotifyListeners(mListeners, [](auto l) { l->OnHistoryGotoIndex(); }); + } + + if (mRequestedIndex == mIndex) { + // Possibly a reload case + InitiateLoad(nextEntry, rootBC, aLoadType, aLoadResults, aLoadCurrentEntry, + aUserActivation, requestedOffset); + return NS_OK; + } + + // Going back or forward. + bool differenceFound = LoadDifferingEntries( + prevEntry, nextEntry, rootBC, aLoadType, aLoadResults, aLoadCurrentEntry, + aUserActivation, requestedOffset); + if (!differenceFound) { + // LoadNextPossibleEntry will change the offset by one, and in order + // to keep track of the requestedOffset, need to reset mRequestedIndex to + // the value it had initially. + mRequestedIndex = originalRequestedIndex; + // We did not find any differences. Go further in the history. + return LoadNextPossibleEntry(aIndex, aLoadType, aHistCmd, aLoadResults, + aLoadCurrentEntry, aUserActivation); + } + + return NS_OK; +} + +bool nsSHistory::LoadDifferingEntries(nsISHEntry* aPrevEntry, + nsISHEntry* aNextEntry, + BrowsingContext* aParent, long aLoadType, + nsTArray<LoadEntryResult>& aLoadResults, + bool aLoadCurrentEntry, + bool aUserActivation, int32_t aOffset) { + MOZ_ASSERT(aPrevEntry && aNextEntry && aParent); + + uint32_t prevID = aPrevEntry->GetID(); + uint32_t nextID = aNextEntry->GetID(); + + // Check the IDs to verify if the pages are different. + if (prevID != nextID) { + // Set the Subframe flag if not navigating the root docshell. + aNextEntry->SetIsSubFrame(aParent->Id() != mRootBC); + InitiateLoad(aNextEntry, aParent, aLoadType, aLoadResults, + aLoadCurrentEntry, aUserActivation, aOffset); + return true; + } + + // The entries are the same, so compare any child frames + int32_t pcnt = aPrevEntry->GetChildCount(); + int32_t ncnt = aNextEntry->GetChildCount(); + + // Create an array for child browsing contexts. + nsTArray<RefPtr<BrowsingContext>> browsingContexts; + aParent->GetChildren(browsingContexts); + + // Search for something to load next. + bool differenceFound = false; + for (int32_t i = 0; i < ncnt; ++i) { + // First get an entry which may cause a new page to be loaded. + nsCOMPtr<nsISHEntry> nChild; + aNextEntry->GetChildAt(i, getter_AddRefs(nChild)); + if (!nChild) { + continue; + } + nsID docshellID; + nChild->GetDocshellID(docshellID); + + // Then find the associated docshell. + RefPtr<BrowsingContext> bcChild; + for (const RefPtr<BrowsingContext>& bc : browsingContexts) { + if (bc->GetHistoryID() == docshellID) { + bcChild = bc; + break; + } + } + if (!bcChild) { + continue; + } + + // Then look at the previous entries to see if there was + // an entry for the docshell. + nsCOMPtr<nsISHEntry> pChild; + for (int32_t k = 0; k < pcnt; ++k) { + nsCOMPtr<nsISHEntry> child; + aPrevEntry->GetChildAt(k, getter_AddRefs(child)); + if (child) { + nsID dID; + child->GetDocshellID(dID); + if (dID == docshellID) { + pChild = child; + break; + } + } + } + if (!pChild) { + continue; + } + + // Finally recursively call this method. + // This will either load a new page to shell or some subshell or + // do nothing. + if (LoadDifferingEntries(pChild, nChild, bcChild, aLoadType, aLoadResults, + aLoadCurrentEntry, aUserActivation, aOffset)) { + differenceFound = true; + } + } + return differenceFound; +} + +void nsSHistory::InitiateLoad(nsISHEntry* aFrameEntry, + BrowsingContext* aFrameBC, long aLoadType, + nsTArray<LoadEntryResult>& aLoadResults, + bool aLoadCurrentEntry, bool aUserActivation, + int32_t aOffset) { + MOZ_ASSERT(aFrameBC && aFrameEntry); + + LoadEntryResult* loadResult = aLoadResults.AppendElement(); + loadResult->mBrowsingContext = aFrameBC; + + nsCOMPtr<nsIURI> newURI = aFrameEntry->GetURI(); + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(newURI); + + loadState->SetHasValidUserGestureActivation(aUserActivation); + + // At the time we initiate a history entry load we already know if https-first + // was able to upgrade the request from http to https. There is no point in + // re-retrying to upgrade. + loadState->SetIsExemptFromHTTPSOnlyMode(true); + + /* Set the loadType in the SHEntry too to what was passed on. + * This will be passed on to child subframes later in nsDocShell, + * so that proper loadType is maintained through out a frameset + */ + aFrameEntry->SetLoadType(aLoadType); + + loadState->SetLoadType(aLoadType); + + loadState->SetSHEntry(aFrameEntry); + + // If we're loading the current entry we want to treat it as not a + // same-document navigation (see nsDocShell::IsSameDocumentNavigation), so + // record that here in the LoadingSessionHistoryEntry. + bool loadingCurrentEntry; + if (mozilla::SessionHistoryInParent()) { + loadingCurrentEntry = aLoadCurrentEntry; + } else { + loadingCurrentEntry = + aFrameBC->GetDocShell() && + nsDocShell::Cast(aFrameBC->GetDocShell())->IsOSHE(aFrameEntry); + } + loadState->SetLoadIsFromSessionHistory(aOffset, loadingCurrentEntry); + + if (mozilla::SessionHistoryInParent()) { + nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aFrameEntry); + aFrameBC->Canonical()->AddLoadingSessionHistoryEntry( + loadState->GetLoadingSessionHistoryInfo()->mLoadId, she); + } + + nsCOMPtr<nsIURI> originalURI = aFrameEntry->GetOriginalURI(); + loadState->SetOriginalURI(originalURI); + + loadState->SetLoadReplace(aFrameEntry->GetLoadReplace()); + + loadState->SetLoadFlags(nsIWebNavigation::LOAD_FLAGS_NONE); + nsCOMPtr<nsIPrincipal> triggeringPrincipal = + aFrameEntry->GetTriggeringPrincipal(); + loadState->SetTriggeringPrincipal(triggeringPrincipal); + loadState->SetFirstParty(false); + nsCOMPtr<nsIContentSecurityPolicy> csp = aFrameEntry->GetCsp(); + loadState->SetCsp(csp); + + loadResult->mLoadState = std::move(loadState); +} + +NS_IMETHODIMP +nsSHistory::CreateEntry(nsISHEntry** aEntry) { + nsCOMPtr<nsISHEntry> entry; + if (XRE_IsParentProcess() && mozilla::SessionHistoryInParent()) { + entry = new SessionHistoryEntry(); + } else { + entry = new nsSHEntry(); + } + entry.forget(aEntry); + return NS_OK; +} + +NS_IMETHODIMP_(bool) +nsSHistory::IsEmptyOrHasEntriesForSingleTopLevelPage() { + if (mEntries.IsEmpty()) { + return true; + } + + nsISHEntry* entry = mEntries[0]; + size_t length = mEntries.Length(); + for (size_t i = 1; i < length; ++i) { + bool sharesDocument = false; + mEntries[i]->SharesDocumentWith(entry, &sharesDocument); + if (!sharesDocument) { + return false; + } + } + + return true; +} + +static void CollectEntries( + nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable, + SessionHistoryEntry* aEntry) { + aHashtable.InsertOrUpdate(aEntry->DocshellID(), aEntry); + for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) { + if (entry) { + CollectEntries(aHashtable, entry); + } + } +} + +static void UpdateEntryLength( + nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable, + SessionHistoryEntry* aNewEntry, bool aMove) { + SessionHistoryEntry* oldEntry = aHashtable.Get(aNewEntry->DocshellID()); + if (oldEntry) { + MOZ_ASSERT(oldEntry->GetID() != aNewEntry->GetID() || !aMove || + !aNewEntry->BCHistoryLength().Modified()); + aNewEntry->SetBCHistoryLength(oldEntry->BCHistoryLength()); + if (oldEntry->GetID() != aNewEntry->GetID()) { + MOZ_ASSERT(!aMove); + // If we have a new id then aNewEntry was created for a new load, so + // record that in BCHistoryLength. + ++aNewEntry->BCHistoryLength(); + } else if (aMove) { + // We're moving the BCHistoryLength from the old entry to the new entry, + // so we need to let the old entry know that it's not contributing to its + // BCHistoryLength, and the new one that it does if the old one was + // before. + aNewEntry->BCHistoryLength().SetModified( + oldEntry->BCHistoryLength().Modified()); + oldEntry->BCHistoryLength().SetModified(false); + } + } + + for (const RefPtr<SessionHistoryEntry>& entry : aNewEntry->Children()) { + if (entry) { + UpdateEntryLength(aHashtable, entry, aMove); + } + } +} + +void nsSHistory::UpdateEntryLength(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry, + bool aMove) { + nsCOMPtr<SessionHistoryEntry> oldSHE = do_QueryInterface(aOldEntry); + nsCOMPtr<SessionHistoryEntry> newSHE = do_QueryInterface(aNewEntry); + + if (!oldSHE || !newSHE) { + return; + } + + nsTHashMap<nsIDHashKey, SessionHistoryEntry*> docshellIDToEntry; + CollectEntries(docshellIDToEntry, oldSHE); + + ::UpdateEntryLength(docshellIDToEntry, newSHE, aMove); +} diff --git a/docshell/shistory/nsSHistory.h b/docshell/shistory/nsSHistory.h new file mode 100644 index 0000000000..d029a55c6c --- /dev/null +++ b/docshell/shistory/nsSHistory.h @@ -0,0 +1,343 @@ +/* -*- 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/. */ + +#ifndef nsSHistory_h +#define nsSHistory_h + +#include "nsCOMPtr.h" +#include "nsDocShellLoadState.h" +#include "nsExpirationTracker.h" +#include "nsISHistory.h" +#include "nsSHEntryShared.h" +#include "nsSimpleEnumerator.h" +#include "nsTObserverArray.h" +#include "nsWeakReference.h" + +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/LinkedList.h" +#include "mozilla/UniquePtr.h" + +class nsIDocShell; +class nsDocShell; +class nsSHistoryObserver; +class nsISHEntry; + +namespace mozilla { +namespace dom { +class LoadSHEntryResult; +} +} // namespace mozilla + +class nsSHistory : public mozilla::LinkedListElement<nsSHistory>, + public nsISHistory, + public nsSupportsWeakReference { + public: + // The timer based history tracker is used to evict bfcache on expiration. + class HistoryTracker final + : public nsExpirationTracker<mozilla::dom::SHEntrySharedParentState, 3> { + public: + explicit HistoryTracker(nsSHistory* aSHistory, uint32_t aTimeout, + nsIEventTarget* aEventTarget) + : nsExpirationTracker(1000 * aTimeout / 2, "HistoryTracker", + aEventTarget) { + MOZ_ASSERT(aSHistory); + mSHistory = aSHistory; + } + + protected: + virtual void NotifyExpired( + mozilla::dom::SHEntrySharedParentState* aObj) override { + RemoveObject(aObj); + mSHistory->EvictExpiredContentViewerForEntry(aObj); + } + + private: + // HistoryTracker is owned by nsSHistory; it always outlives HistoryTracker + // so it's safe to use raw pointer here. + nsSHistory* mSHistory; + }; + + // Structure used in SetChildHistoryEntry + struct SwapEntriesData { + mozilla::dom::BrowsingContext* + ignoreBC; // constant; the browsing context to ignore + nsISHEntry* destTreeRoot; // constant; the root of the dest tree + nsISHEntry* destTreeParent; // constant; the node under destTreeRoot + // whose children will correspond to aEntry + }; + + explicit nsSHistory(mozilla::dom::BrowsingContext* aRootBC); + NS_DECL_ISUPPORTS + NS_DECL_NSISHISTORY + + // One time initialization method + static nsresult Startup(); + static void Shutdown(); + static void UpdatePrefs(); + + // Max number of total cached content viewers. If the pref + // browser.sessionhistory.max_total_viewers is negative, then + // this value is calculated based on the total amount of memory. + // Otherwise, it comes straight from the pref. + static uint32_t GetMaxTotalViewers() { return sHistoryMaxTotalViewers; } + + // Get the root SHEntry from a given entry. + static already_AddRefed<nsISHEntry> GetRootSHEntry(nsISHEntry* aEntry); + + // Callback prototype for WalkHistoryEntries. + // `aEntry` is the child history entry, `aBC` is its corresponding browsing + // context, `aChildIndex` is the child's index in its parent entry, and + // `aData` is the opaque pointer passed to WalkHistoryEntries. Both structs + // that are passed as `aData` to this function have a field + // `aEntriesToUpdate`, which is an array of entries we need to update in + // docshell, if the 'SH in parent' pref is on (which implies that this method + // is executed in the parent) + typedef nsresult (*WalkHistoryEntriesFunc)(nsISHEntry* aEntry, + mozilla::dom::BrowsingContext* aBC, + int32_t aChildIndex, void* aData); + + // Clone a session history tree for subframe navigation. + // The tree rooted at |aSrcEntry| will be cloned into |aDestEntry|, except + // for the entry with id |aCloneID|, which will be replaced with + // |aReplaceEntry|. |aSrcShell| is a (possibly null) docshell which + // corresponds to |aSrcEntry| via its mLSHE or mOHE pointers, and will + // have that pointer updated to point to the cloned history entry. + // If aCloneChildren is true then the children of the entry with id + // |aCloneID| will be cloned into |aReplaceEntry|. + static nsresult CloneAndReplace(nsISHEntry* aSrcEntry, + mozilla::dom::BrowsingContext* aOwnerBC, + uint32_t aCloneID, nsISHEntry* aReplaceEntry, + bool aCloneChildren, nsISHEntry** aDestEntry); + + // Child-walking callback for CloneAndReplace + static nsresult CloneAndReplaceChild(nsISHEntry* aEntry, + mozilla::dom::BrowsingContext* aOwnerBC, + int32_t aChildIndex, void* aData); + + // Child-walking callback for SetHistoryEntry + static nsresult SetChildHistoryEntry(nsISHEntry* aEntry, + mozilla::dom::BrowsingContext* aBC, + int32_t aEntryIndex, void* aData); + + // For each child of aRootEntry, find the corresponding shell which is + // a child of aBC, and call aCallback. The opaque pointer aData + // is passed to the callback. + static nsresult WalkHistoryEntries(nsISHEntry* aRootEntry, + mozilla::dom::BrowsingContext* aBC, + WalkHistoryEntriesFunc aCallback, + void* aData); + + // This function finds all entries that are contiguous and same-origin with + // the aEntry. And call the aCallback on them, including the aEntry. This only + // works for the root entries. It will do nothing for non-root entries. + static void WalkContiguousEntries( + nsISHEntry* aEntry, const std::function<void(nsISHEntry*)>& aCallback); + + nsTArray<nsCOMPtr<nsISHEntry>>& Entries() { return mEntries; } + + void NotifyOnHistoryReplaceEntry(); + + void RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex, + bool* aDidRemove); + + // The size of the window of SHEntries which can have alive viewers in the + // bfcache around the currently active SHEntry. + // + // We try to keep viewers for SHEntries between index - VIEWER_WINDOW and + // index + VIEWER_WINDOW alive. + static const int32_t VIEWER_WINDOW = 3; + + struct LoadEntryResult { + RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext; + RefPtr<nsDocShellLoadState> mLoadState; + }; + + static void LoadURIs(nsTArray<LoadEntryResult>& aLoadResults); + static void LoadURIOrBFCache(LoadEntryResult& aLoadEntry); + + // If this doesn't return an error then either aLoadResult is set to nothing, + // in which case the caller should ignore the load, or it returns a valid + // LoadEntryResult in aLoadResult which the caller should use to do the load. + nsresult Reload(uint32_t aReloadFlags, + nsTArray<LoadEntryResult>& aLoadResults); + nsresult ReloadCurrentEntry(nsTArray<LoadEntryResult>& aLoadResults); + nsresult GotoIndex(int32_t aIndex, nsTArray<LoadEntryResult>& aLoadResults, + bool aSameEpoch, bool aLoadCurrentEntry, + bool aUserActivation); + + void WindowIndices(int32_t aIndex, int32_t* aOutStartIndex, + int32_t* aOutEndIndex); + void NotifyListenersContentViewerEvicted(uint32_t aNumEvicted); + + int32_t Length() { return int32_t(mEntries.Length()); } + int32_t Index() { return mIndex; } + already_AddRefed<mozilla::dom::BrowsingContext> GetBrowsingContext() { + return mozilla::dom::BrowsingContext::Get(mRootBC); + } + bool HasOngoingUpdate() { return mHasOngoingUpdate; } + void SetHasOngoingUpdate(bool aVal) { mHasOngoingUpdate = aVal; } + + void SetBrowsingContext(mozilla::dom::BrowsingContext* aRootBC) { + uint64_t newID = aRootBC ? aRootBC->Id() : 0; + if (mRootBC != newID) { + mRootBC = newID; + UpdateRootBrowsingContextState(aRootBC); + } + } + + int32_t GetIndexForReplace() { + // Replace current entry in session history; If the requested index is + // valid, it indicates the loading was triggered by a history load, and + // we should replace the entry at requested index instead. + return mRequestedIndex == -1 ? mIndex : mRequestedIndex; + } + + // Update the root browsing context state when adding, removing or + // replacing entries. + void UpdateRootBrowsingContextState() { + RefPtr<mozilla::dom::BrowsingContext> rootBC(GetBrowsingContext()); + UpdateRootBrowsingContextState(rootBC); + } + + void GetEpoch(uint64_t& aEpoch, + mozilla::Maybe<mozilla::dom::ContentParentId>& aId) const { + aEpoch = mEpoch; + aId = mEpochParentId; + } + void SetEpoch(uint64_t aEpoch, + mozilla::Maybe<mozilla::dom::ContentParentId> aId) { + mEpoch = aEpoch; + mEpochParentId = aId; + } + + void LogHistory(); + + protected: + virtual ~nsSHistory(); + + uint64_t mRootBC; + + private: + friend class nsSHistoryObserver; + + void UpdateRootBrowsingContextState( + mozilla::dom::BrowsingContext* aBrowsingContext); + + bool LoadDifferingEntries(nsISHEntry* aPrevEntry, nsISHEntry* aNextEntry, + mozilla::dom::BrowsingContext* aParent, + long aLoadType, + nsTArray<LoadEntryResult>& aLoadResults, + bool aLoadCurrentEntry, bool aUserActivation, + int32_t aOffset); + void InitiateLoad(nsISHEntry* aFrameEntry, + mozilla::dom::BrowsingContext* aFrameBC, long aLoadType, + nsTArray<LoadEntryResult>& aLoadResult, + bool aLoadCurrentEntry, bool aUserActivation, + int32_t aOffset); + + nsresult LoadEntry(int32_t aIndex, long aLoadType, uint32_t aHistCmd, + nsTArray<LoadEntryResult>& aLoadResults, bool aSameEpoch, + bool aLoadCurrentEntry, bool aUserActivation); + + // Find the history entry for a given bfcache entry. It only looks up between + // the range where alive viewers may exist (i.e nsSHistory::VIEWER_WINDOW). + nsresult FindEntryForBFCache(mozilla::dom::SHEntrySharedParentState* aEntry, + nsISHEntry** aResult, int32_t* aResultIndex); + + // Evict content viewers in this window which don't lie in the "safe" range + // around aIndex. + virtual void EvictOutOfRangeWindowContentViewers(int32_t aIndex); + void EvictContentViewerForEntry(nsISHEntry* aEntry); + static void GloballyEvictContentViewers(); + static void GloballyEvictAllContentViewers(); + + // Calculates a max number of total + // content viewers to cache, based on amount of total memory + static uint32_t CalcMaxTotalViewers(); + + nsresult LoadNextPossibleEntry(int32_t aNewIndex, long aLoadType, + uint32_t aHistCmd, + nsTArray<LoadEntryResult>& aLoadResults, + bool aLoadCurrentEntry, bool aUserActivation); + + // aIndex is the index of the entry which may be removed. + // If aKeepNext is true, aIndex is compared to aIndex + 1, + // otherwise comparison is done to aIndex - 1. + bool RemoveDuplicate(int32_t aIndex, bool aKeepNext); + + // We need to update entries in docshell and browsing context. + // If our docshell is located in parent or 'SH in parent' pref is off we can + // update it directly, Otherwise, we have two choices. If the browsing context + // that owns the docshell is in the same process as the process who called us + // over IPC, then we save entries that need to be updated in a list, and once + // we have returned from the IPC call, we update the docshell in the child + // process. Otherwise, if the browsing context is in a different process, we + // do a nested IPC call to that process to update the docshell in that + // process. + static void HandleEntriesToSwapInDocShell(mozilla::dom::BrowsingContext* aBC, + nsISHEntry* aOldEntry, + nsISHEntry* aNewEntry); + + void UpdateEntryLength(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry, + bool aMove); + + protected: + bool mHasOngoingUpdate; + nsTArray<nsCOMPtr<nsISHEntry>> mEntries; // entries are never null + private: + // Track all bfcache entries and evict on expiration. + mozilla::UniquePtr<HistoryTracker> mHistoryTracker; + + int32_t mIndex; // -1 means "no index" + int32_t mRequestedIndex; // -1 means "no requested index" + + // Session History listeners + nsAutoTObserverArray<nsWeakPtr, 2> mListeners; + + nsID mRootDocShellID; + + // Max viewers allowed total, across all SHistory objects + static int32_t sHistoryMaxTotalViewers; + + // The epoch (and id) tell us what navigations occured within the same + // event-loop spin in the child. We need to know this in order to + // implement spec requirements for dropping pending navigations when we + // do a history navigation, if it's not same-document. Content processes + // update the epoch via a runnable on each ::Go (including AsyncGo). + uint64_t mEpoch = 0; + mozilla::Maybe<mozilla::dom::ContentParentId> mEpochParentId; +}; + +// CallerWillNotifyHistoryIndexAndLengthChanges is used to prevent +// SHistoryChangeNotifier to send automatic index and length updates. +// When that is done, it is up to the caller to explicitly send those updates. +// This is needed in cases when the update is a reaction to some change in a +// child process and child process passes a changeId to the parent side. +class MOZ_STACK_CLASS CallerWillNotifyHistoryIndexAndLengthChanges { + public: + explicit CallerWillNotifyHistoryIndexAndLengthChanges( + nsISHistory* aSHistory) { + nsSHistory* shistory = static_cast<nsSHistory*>(aSHistory); + if (shistory && !shistory->HasOngoingUpdate()) { + shistory->SetHasOngoingUpdate(true); + mSHistory = shistory; + } + } + + ~CallerWillNotifyHistoryIndexAndLengthChanges() { + if (mSHistory) { + mSHistory->SetHasOngoingUpdate(false); + } + } + + RefPtr<nsSHistory> mSHistory; +}; + +inline nsISupports* ToSupports(nsSHistory* aObj) { + return static_cast<nsISHistory*>(aObj); +} + +#endif /* nsSHistory */ diff --git a/docshell/test/browser/Bug1622420Child.sys.mjs b/docshell/test/browser/Bug1622420Child.sys.mjs new file mode 100644 index 0000000000..c5520d5943 --- /dev/null +++ b/docshell/test/browser/Bug1622420Child.sys.mjs @@ -0,0 +1,9 @@ +export class Bug1622420Child extends JSWindowActorChild { + receiveMessage(msg) { + switch (msg.name) { + case "hasWindowContextForTopBC": + return !!this.browsingContext.top.currentWindowContext; + } + return null; + } +} diff --git a/docshell/test/browser/Bug422543Child.sys.mjs b/docshell/test/browser/Bug422543Child.sys.mjs new file mode 100644 index 0000000000..524ac33ffd --- /dev/null +++ b/docshell/test/browser/Bug422543Child.sys.mjs @@ -0,0 +1,98 @@ +class SHistoryListener { + constructor() { + this.retval = true; + this.last = "initial"; + } + + OnHistoryNewEntry(aNewURI) { + this.last = "newentry"; + } + + OnHistoryGotoIndex() { + this.last = "gotoindex"; + } + + OnHistoryPurge() { + this.last = "purge"; + } + + OnHistoryReload() { + this.last = "reload"; + return this.retval; + } + + OnHistoryReplaceEntry() {} +} +SHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", +]); + +let listeners; + +export class Bug422543Child extends JSWindowActorChild { + constructor() { + super(); + } + + actorCreated() { + if (listeners) { + return; + } + + this.shistory = this.docShell.nsIWebNavigation.sessionHistory; + listeners = [new SHistoryListener(), new SHistoryListener()]; + + for (let listener of listeners) { + this.shistory.legacySHistory.addSHistoryListener(listener); + } + } + + cleanup() { + for (let listener of listeners) { + this.shistory.legacySHistory.removeSHistoryListener(listener); + } + this.shistory = null; + listeners = null; + return {}; + } + + getListenerStatus() { + return listeners.map(l => l.last); + } + + resetListeners() { + for (let listener of listeners) { + listener.last = "initial"; + } + + return {}; + } + + notifyReload() { + let history = this.shistory.legacySHistory; + let rval = history.notifyOnHistoryReload(); + return { rval }; + } + + setRetval({ num, val }) { + listeners[num].retval = val; + return {}; + } + + receiveMessage(msg) { + switch (msg.name) { + case "cleanup": + return this.cleanup(); + case "getListenerStatus": + return this.getListenerStatus(); + case "notifyReload": + return this.notifyReload(); + case "resetListeners": + return this.resetListeners(); + case "setRetval": + return this.setRetval(msg.data); + } + return null; + } +} diff --git a/docshell/test/browser/browser.ini b/docshell/test/browser/browser.ini new file mode 100644 index 0000000000..fc3e06d9a3 --- /dev/null +++ b/docshell/test/browser/browser.ini @@ -0,0 +1,237 @@ +[DEFAULT] +support-files = + Bug422543Child.sys.mjs + dummy_page.html + favicon_bug655270.ico + file_bug234628-1-child.html + file_bug234628-1.html + file_bug234628-10-child.xhtml + file_bug234628-10.html + file_bug234628-11-child.xhtml + file_bug234628-11-child.xhtml^headers^ + file_bug234628-11.html + file_bug234628-2-child.html + file_bug234628-2.html + file_bug234628-3-child.html + file_bug234628-3.html + file_bug234628-4-child.html + file_bug234628-4.html + file_bug234628-5-child.html + file_bug234628-5.html + file_bug234628-6-child.html + file_bug234628-6-child.html^headers^ + file_bug234628-6.html + file_bug234628-8-child.html + file_bug234628-8.html + file_bug234628-9-child.html + file_bug234628-9.html + file_bug420605.html + file_bug503832.html + file_bug655270.html + file_bug670318.html + file_bug673087-1.html + file_bug673087-1.html^headers^ + file_bug673087-1-child.html + file_bug673087-2.html + file_bug852909.pdf + file_bug852909.png + file_bug1046022.html + file_bug1206879.html + file_bug1328501.html + file_bug1328501_frame.html + file_bug1328501_framescript.js + file_bug1543077-3-child.html + file_bug1543077-3.html + file_multiple_pushState.html + file_onbeforeunload_0.html + file_onbeforeunload_1.html + file_onbeforeunload_2.html + file_onbeforeunload_3.html + print_postdata.sjs + test-form_sjis.html + browser_timelineMarkers-frame-02.js + head.js + frame-head.js + file_data_load_inherit_csp.html + file_click_link_within_view_source.html + onload_message.html + onpageshow_message.html + file_cross_process_csp_inheritance.html + file_open_about_blank.html + file_slow_load.sjs + file_bug1648464-1.html + file_bug1648464-1-child.html + file_bug1688368-1.sjs + file_bug1691153.html + file_bug1716290-1.sjs + file_bug1716290-2.sjs + file_bug1716290-3.sjs + file_bug1716290-4.sjs + file_bug1736248-1.html + +[browser_alternate_fixup_middle_click_link.js] +https_first_disabled = true +[browser_backforward_userinteraction.js] +support-files = + dummy_iframe_page.html +skip-if = + os == "linux" && bits == 64 && !debug # Bug 1607713 +[browser_backforward_userinteraction_about.js] +[browser_backforward_userinteraction_systemprincipal.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_bug1543077-3.js] +[browser_bug1594938.js] +[browser_bug1206879.js] +https_first_disabled = true +[browser_bug1309900_crossProcessHistoryNavigation.js] +https_first_disabled = true +[browser_bug1328501.js] +https_first_disabled = true +[browser_bug1347823.js] +[browser_bug134911.js] +[browser_bug1415918_beforeunload_options.js] +https_first_disabled = true +[browser_bug1622420.js] +support-files = + file_bug1622420.html + Bug1622420Child.sys.mjs +[browser_bug1673702.js] +https_first_disabled = true +skip-if = + os == "linux" && bits == 64 && os_version == "18.04" && debug # Bug 1674513 + os == "win" # Bug 1674513 +support-files = + file_bug1673702.json + file_bug1673702.json^headers^ +[browser_bug1674464.js] +https_first_disabled = true +skip-if = !fission || !crashreporter # On a crash we only keep history when fission is enabled. +[browser_bug1719178.js] +[browser_bug1757005.js] +[browser_bug1769189.js] +[browser_bug1798780.js] +[browser_bug234628-1.js] +[browser_bug234628-10.js] +[browser_bug234628-11.js] +[browser_bug234628-2.js] +[browser_bug234628-3.js] +[browser_bug234628-4.js] +[browser_bug234628-5.js] +[browser_bug234628-6.js] +[browser_bug234628-8.js] +[browser_bug234628-9.js] +[browser_bug349769.js] +[browser_bug388121-1.js] +[browser_bug388121-2.js] +[browser_bug420605.js] +skip-if = verify +[browser_bug422543.js] +https_first_disabled = true +[browser_bug441169.js] +[browser_bug503832.js] +skip-if = verify +[browser_bug554155.js] +[browser_bug655270.js] +[browser_bug655273.js] +[browser_bug670318.js] +skip-if = os == "linux" && (debug || asan || tsan) # Bug 1717403 +[browser_bug673087-1.js] +[browser_bug673087-2.js] +[browser_bug673467.js] +[browser_bug852909.js] +skip-if = (verify && debug && (os == 'win')) +[browser_bug92473.js] +[browser_csp_sandbox_no_script_js_uri.js] +support-files = + file_csp_sandbox_no_script_js_uri.html + file_csp_sandbox_no_script_js_uri.html^headers^ +[browser_data_load_inherit_csp.js] +[browser_dataURI_unique_opaque_origin.js] +https_first_disabled = true +[browser_frameloader_swap_with_bfcache.js] +[browser_backforward_restore_scroll.js] +https_first_disabled = true +support-files = + file_backforward_restore_scroll.html + file_backforward_restore_scroll.html^headers^ +[browser_targetTopLevelLinkClicksToBlank.js] +[browser_title_in_session_history.js] +skip-if = !sessionHistoryInParent +[browser_uriFixupIntegration.js] +[browser_uriFixupAlternateRedirects.js] +https_first_disabled = true +support-files = + redirect_to_example.sjs +[browser_loadURI_postdata.js] +[browser_multiple_pushState.js] +https_first_disabled = true +[browser_onbeforeunload_frame.js] +support-files = head_browser_onbeforeunload.js +[browser_onbeforeunload_parent.js] +support-files = head_browser_onbeforeunload.js +[browser_onbeforeunload_navigation.js] +skip-if = (os == 'win' && !debug) # bug 1300351 +[browser_onunload_stop.js] +https_first_disabled = true +[browser_overlink.js] +support-files = + overlink_test.html +[browser_platform_emulation.js] +[browser_search_notification.js] +[browser_tab_touch_events.js] +[browser_timelineMarkers-01.js] +[browser_timelineMarkers-02.js] +skip-if = true # Bug 1220415 +[browser_ua_emulation.js] +[browser_history_triggeringprincipal_viewsource.js] +https_first_disabled = true +[browser_click_link_within_view_source.js] +[browser_browsingContext-01.js] +https_first_disabled = true +[browser_browsingContext-02.js] +https_first_disabled = true +[browser_browsingContext-getAllBrowsingContextsInSubtree.js] +[browser_browsingContext-getWindowByName.js] +[browser_browsingContext-embedder.js] +[browser_browsingContext-webProgress.js] +skip-if = + os == "linux" && bits == 64 && !debug # Bug 1721261 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +https_first_disabled = true +[browser_csp_uir.js] +support-files = + file_csp_uir.html + file_csp_uir_dummy.html +[browser_cross_process_csp_inheritance.js] +https_first_disabled = true +[browser_tab_replace_while_loading.js] +skip-if = (os == 'linux' && bits == 64 && os_version == '18.04') || (os == "win") # Bug 1604237, Bug 1671794 +[browser_browsing_context_attached.js] +https_first_disabled = true +[browser_browsing_context_discarded.js] +https_first_disabled = true +[browser_fall_back_to_https.js] +https_first_disabled = true +skip-if = (os == 'mac') +[browser_badCertDomainFixup.js] +[browser_viewsource_chrome_to_content.js] +[browser_viewsource_multipart.js] +support-files = + file_basic_multipart.sjs +[browser_bug1648464-1.js] +[browser_bug1688368-1.js] +[browser_bug1691153.js] +https_first_disabled = true +[browser_bug1705872.js] +[browser_isInitialDocument.js] +https_first_disabled = true +[browser_bug1716290-1.js] +[browser_bug1716290-2.js] +[browser_bug1716290-3.js] +[browser_bug1716290-4.js] +[browser_bfcache_copycommand.js] +skip-if = + os == "linux" && bits == 64 # Bug 1730593 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_bug1736248-1.js] diff --git a/docshell/test/browser/browser_alternate_fixup_middle_click_link.js b/docshell/test/browser/browser_alternate_fixup_middle_click_link.js new file mode 100644 index 0000000000..8a55e12e52 --- /dev/null +++ b/docshell/test/browser/browser_alternate_fixup_middle_click_link.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that we don't do alternate fixup when users middle-click + * broken links in the content document. + */ +add_task(async function test_alt_fixup_middle_click() { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + await SpecialPowers.spawn(browser, [], () => { + let link = content.document.createElement("a"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + link.href = "http://example/foo"; + link.textContent = "Me, me, click me!"; + content.document.body.append(link); + }); + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "a[href]", + { button: 1 }, + browser + ); + let tab = await newTabPromise; + let { browsingContext } = tab.linkedBrowser; + // Account for the possibility of a race, where the error page has already loaded: + if ( + !browsingContext.currentWindowGlobal?.documentURI.spec.startsWith( + "about:neterror" + ) + ) { + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + null, + true + ); + } + // TBH, if the test fails, we probably force-crash because we try to reach + // *www.* example.com, which isn't proxied by the test infrastructure so + // will forcibly abort the test. But we need some asserts so they might as + // well be meaningful: + is( + tab.linkedBrowser.currentURI.spec, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example/foo", + "URL for tab should be correct." + ); + + ok( + browsingContext.currentWindowGlobal.documentURI.spec.startsWith( + "about:neterror" + ), + "Should be showing error page." + ); + BrowserTestUtils.removeTab(tab); + }); +}); diff --git a/docshell/test/browser/browser_backforward_restore_scroll.js b/docshell/test/browser/browser_backforward_restore_scroll.js new file mode 100644 index 0000000000..2d1dfe624b --- /dev/null +++ b/docshell/test/browser/browser_backforward_restore_scroll.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://mochi.test:8888" +); +const URL1 = ROOT + "file_backforward_restore_scroll.html"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const URL2 = "http://example.net/"; + +const SCROLL0 = 500; +const SCROLL1 = 1000; + +function promiseBrowserLoaded(url) { + return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, url); +} + +add_task(async function test() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, URL1); + + // Scroll the 2 frames. + let children = gBrowser.selectedBrowser.browsingContext.children; + await SpecialPowers.spawn(children[0], [SCROLL0], scrollY => + content.scrollTo(0, scrollY) + ); + await SpecialPowers.spawn(children[1], [SCROLL1], scrollY => + content.scrollTo(0, scrollY) + ); + + // Navigate forwards then backwards. + let loaded = promiseBrowserLoaded(URL2); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, URL2); + await loaded; + + loaded = promiseBrowserLoaded(URL1); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.history.back(); + }); + await loaded; + + // And check the results. + children = gBrowser.selectedBrowser.browsingContext.children; + await SpecialPowers.spawn(children[0], [SCROLL0], scrollY => { + Assert.equal(content.scrollY, scrollY, "frame 0 has correct scroll"); + }); + await SpecialPowers.spawn(children[1], [SCROLL1], scrollY => { + Assert.equal(content.scrollY, scrollY, "frame 1 has correct scroll"); + }); + + gBrowser.removeTab(gBrowser.selectedTab); +}); diff --git a/docshell/test/browser/browser_backforward_userinteraction.js b/docshell/test/browser/browser_backforward_userinteraction.js new file mode 100644 index 0000000000..264299c902 --- /dev/null +++ b/docshell/test/browser/browser_backforward_userinteraction.js @@ -0,0 +1,374 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "dummy_page.html"; +const IFRAME_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "dummy_iframe_page.html"; + +async function assertMenulist(entries, baseURL = TEST_PAGE) { + // Wait for the session data to be flushed before continuing the test + await new Promise(resolve => + SessionStore.getSessionHistory(gBrowser.selectedTab, resolve) + ); + + let backButton = document.getElementById("back-button"); + let contextMenu = document.getElementById("backForwardMenu"); + + info("waiting for the history menu to open"); + + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(backButton, { + type: "contextmenu", + button: 2, + }); + await popupShownPromise; + + ok(true, "history menu opened"); + + let nodes = contextMenu.childNodes; + + is( + nodes.length, + entries.length, + "Has the expected number of contextMenu entries" + ); + + for (let i = 0; i < entries.length; i++) { + let node = nodes[i]; + is( + node.getAttribute("uri").replace(/[?|#]/, "!"), + baseURL + "!entry=" + entries[i], + "contextMenu node has the correct uri" + ); + } + + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; +} + +// There are different ways of loading a page, but they should exhibit roughly the same +// back-forward behavior for the purpose of requiring user interaction. Thus, we +// have a utility function that runs the same test with a parameterized method of loading +// new URLs. +async function runTopLevelTest(loadMethod, useHashes = false) { + let p = useHashes ? "#" : "?"; + + // Test with both pref on and off + for (let requireUserInteraction of [true, false]) { + Services.prefs.setBoolPref( + "browser.navigation.requireUserInteraction", + requireUserInteraction + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE + p + "entry=0" + ); + let browser = tab.linkedBrowser; + + assertBackForwardState(false, false); + + await loadMethod(TEST_PAGE + p + "entry=1"); + + assertBackForwardState(true, false); + await assertMenulist([1, 0]); + + await loadMethod(TEST_PAGE + p + "entry=2"); + + assertBackForwardState(true, false); + await assertMenulist(requireUserInteraction ? [2, 0] : [2, 1, 0]); + + await loadMethod(TEST_PAGE + p + "entry=3"); + + info("Adding user interaction for entry=3"); + // Add some user interaction to entry 3 + await BrowserTestUtils.synthesizeMouse( + "body", + 0, + 0, + {}, + browser.browsingContext, + true + ); + + assertBackForwardState(true, false); + await assertMenulist(requireUserInteraction ? [3, 0] : [3, 2, 1, 0]); + + await loadMethod(TEST_PAGE + p + "entry=4"); + + assertBackForwardState(true, false); + await assertMenulist(requireUserInteraction ? [4, 3, 0] : [4, 3, 2, 1, 0]); + + info("Adding user interaction for entry=4"); + // Add some user interaction to entry 4 + await BrowserTestUtils.synthesizeMouse( + "body", + 0, + 0, + {}, + browser.browsingContext, + true + ); + + await loadMethod(TEST_PAGE + p + "entry=5"); + + assertBackForwardState(true, false); + await assertMenulist( + requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0] + ); + + await goBack(TEST_PAGE + p + "entry=4"); + await goBack(TEST_PAGE + p + "entry=3"); + + if (!requireUserInteraction) { + await goBack(TEST_PAGE + p + "entry=2"); + await goBack(TEST_PAGE + p + "entry=1"); + } + + assertBackForwardState(true, true); + await assertMenulist( + requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0] + ); + + await goBack(TEST_PAGE + p + "entry=0"); + + assertBackForwardState(false, true); + + if (!requireUserInteraction) { + await goForward(TEST_PAGE + p + "entry=1"); + await goForward(TEST_PAGE + p + "entry=2"); + } + + await goForward(TEST_PAGE + p + "entry=3"); + + assertBackForwardState(true, true); + await assertMenulist( + requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0] + ); + + await goForward(TEST_PAGE + p + "entry=4"); + + assertBackForwardState(true, true); + await assertMenulist( + requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0] + ); + + await goForward(TEST_PAGE + p + "entry=5"); + + assertBackForwardState(true, false); + await assertMenulist( + requireUserInteraction ? [5, 4, 3, 0] : [5, 4, 3, 2, 1, 0] + ); + + BrowserTestUtils.removeTab(tab); + } + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); +} + +async function runIframeTest(loadMethod) { + // Test with both pref on and off + for (let requireUserInteraction of [true, false]) { + Services.prefs.setBoolPref( + "browser.navigation.requireUserInteraction", + requireUserInteraction + ); + + // First test the boring case where we only have one iframe. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + IFRAME_PAGE + "?entry=0" + ); + let browser = tab.linkedBrowser; + + assertBackForwardState(false, false); + + await loadMethod(TEST_PAGE + "?sub_entry=1", "frame1"); + + assertBackForwardState(true, false); + await assertMenulist([0, 0], IFRAME_PAGE); + + await loadMethod(TEST_PAGE + "?sub_entry=2", "frame1"); + + assertBackForwardState(true, false); + await assertMenulist( + requireUserInteraction ? [0, 0] : [0, 0, 0], + IFRAME_PAGE + ); + + let bc = await SpecialPowers.spawn(browser, [], function () { + return content.document.getElementById("frame1").browsingContext; + }); + + // Add some user interaction to sub entry 2 + await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, bc, true); + + await loadMethod(TEST_PAGE + "?sub_entry=3", "frame1"); + + assertBackForwardState(true, false); + await assertMenulist( + requireUserInteraction ? [0, 0, 0] : [0, 0, 0, 0], + IFRAME_PAGE + ); + + await loadMethod(TEST_PAGE + "?sub_entry=4", "frame1"); + + assertBackForwardState(true, false); + await assertMenulist( + requireUserInteraction ? [0, 0, 0] : [0, 0, 0, 0, 0], + IFRAME_PAGE + ); + + if (!requireUserInteraction) { + await goBack(TEST_PAGE + "?sub_entry=3", true); + } + + await goBack(TEST_PAGE + "?sub_entry=2", true); + + assertBackForwardState(true, true); + await assertMenulist( + requireUserInteraction ? [0, 0, 0] : [0, 0, 0, 0, 0], + IFRAME_PAGE + ); + + await loadMethod(IFRAME_PAGE + "?entry=1"); + + assertBackForwardState(true, false); + await assertMenulist( + requireUserInteraction ? [1, 0, 0] : [1, 0, 0, 0], + IFRAME_PAGE + ); + + BrowserTestUtils.removeTab(tab); + + // Two iframes, now we're talking. + tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + IFRAME_PAGE + "?entry=0" + ); + browser = tab.linkedBrowser; + + await loadMethod(IFRAME_PAGE + "?entry=1"); + + assertBackForwardState(true, false); + await assertMenulist(requireUserInteraction ? [1, 0] : [1, 0], IFRAME_PAGE); + + // Add some user interaction to frame 1. + bc = await SpecialPowers.spawn(browser, [], function () { + return content.document.getElementById("frame1").browsingContext; + }); + await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, bc, true); + + // Add some user interaction to frame 2. + bc = await SpecialPowers.spawn(browser, [], function () { + return content.document.getElementById("frame2").browsingContext; + }); + await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, bc, true); + + // Navigate frame 2. + await loadMethod(TEST_PAGE + "?sub_entry=1", "frame2"); + + assertBackForwardState(true, false); + await assertMenulist( + requireUserInteraction ? [1, 1, 0] : [1, 1, 0], + IFRAME_PAGE + ); + + // Add some user interaction to frame 1, again. + bc = await SpecialPowers.spawn(browser, [], function () { + return content.document.getElementById("frame1").browsingContext; + }); + await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, bc, true); + + // Navigate frame 2, again. + await loadMethod(TEST_PAGE + "?sub_entry=2", "frame2"); + + assertBackForwardState(true, false); + await assertMenulist( + requireUserInteraction ? [1, 1, 1, 0] : [1, 1, 1, 0], + IFRAME_PAGE + ); + + await goBack(TEST_PAGE + "?sub_entry=1", true); + + assertBackForwardState(true, true); + await assertMenulist( + requireUserInteraction ? [1, 1, 1, 0] : [1, 1, 1, 0], + IFRAME_PAGE + ); + + await goBack(TEST_PAGE + "?sub_entry=0", true); + + assertBackForwardState(true, true); + await assertMenulist( + requireUserInteraction ? [1, 1, 1, 0] : [1, 1, 1, 0], + IFRAME_PAGE + ); + + BrowserTestUtils.removeTab(tab); + } + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); +} + +// Test that when the pref is flipped, we are skipping history +// entries without user interaction when following links with hash URIs. +add_task(async function test_hashURI() { + async function followLinkHash(url) { + info(`Creating and following a link to ${url}`); + let browser = gBrowser.selectedBrowser; + let loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url); + await SpecialPowers.spawn(browser, [url], function (url) { + let a = content.document.createElement("a"); + a.href = url; + content.document.body.appendChild(a); + a.click(); + }); + await loaded; + info(`Loaded ${url}`); + } + + await runTopLevelTest(followLinkHash, true); +}); + +// Test that when the pref is flipped, we are skipping history +// entries without user interaction when using history.pushState. +add_task(async function test_pushState() { + await runTopLevelTest(pushState); +}); + +// Test that when the pref is flipped, we are skipping history +// entries without user interaction when following a link. +add_task(async function test_followLink() { + await runTopLevelTest(followLink); +}); + +// Test that when the pref is flipped, we are skipping history +// entries without user interaction when navigating inside an iframe +// using history.pushState. +add_task(async function test_iframe_pushState() { + await runIframeTest(pushState); +}); + +// Test that when the pref is flipped, we are skipping history +// entries without user interaction when navigating inside an iframe +// by following links. +add_task(async function test_iframe_followLink() { + await runIframeTest(followLink); +}); diff --git a/docshell/test/browser/browser_backforward_userinteraction_about.js b/docshell/test/browser/browser_backforward_userinteraction_about.js new file mode 100644 index 0000000000..606fcc45c5 --- /dev/null +++ b/docshell/test/browser/browser_backforward_userinteraction_about.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "dummy_page.html"; + +// Regression test for navigating back after visiting an about: page +// loaded in the parent process. +add_task(async function test_about_back() { + // Test with both pref on and off + for (let requireUserInteraction of [true, false]) { + Services.prefs.setBoolPref( + "browser.navigation.requireUserInteraction", + requireUserInteraction + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE + "?entry=0" + ); + let browser = tab.linkedBrowser; + assertBackForwardState(false, false); + + await followLink(TEST_PAGE + "?entry=1"); + assertBackForwardState(true, false); + + await followLink(TEST_PAGE + "?entry=2"); + assertBackForwardState(true, false); + + // Add some user interaction to entry 2 + await BrowserTestUtils.synthesizeMouse("body", 0, 0, {}, browser, true); + + await loadURI("about:config"); + assertBackForwardState(true, false); + + await goBack(TEST_PAGE + "?entry=2"); + assertBackForwardState(true, true); + + if (!requireUserInteraction) { + await goBack(TEST_PAGE + "?entry=1"); + assertBackForwardState(true, true); + } + + await goBack(TEST_PAGE + "?entry=0"); + assertBackForwardState(false, true); + + if (!requireUserInteraction) { + await goForward(TEST_PAGE + "?entry=1"); + assertBackForwardState(true, true); + } + + await goForward(TEST_PAGE + "?entry=2"); + assertBackForwardState(true, true); + + await goForward("about:config"); + assertBackForwardState(true, false); + + BrowserTestUtils.removeTab(tab); + } + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); +}); diff --git a/docshell/test/browser/browser_backforward_userinteraction_systemprincipal.js b/docshell/test/browser/browser_backforward_userinteraction_systemprincipal.js new file mode 100644 index 0000000000..289ed1d133 --- /dev/null +++ b/docshell/test/browser/browser_backforward_userinteraction_systemprincipal.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "dummy_page.html"; + +async function runTest(privilegedLoad) { + let prefVals; + // Test with both pref on and off, unless parent-controlled pref is enabled. + // This distinction can be removed once SHIP is enabled by default. + if ( + Services.prefs.getBoolPref("browser.tabs.documentchannel.parent-controlled") + ) { + prefVals = [false]; + } else { + prefVals = [true, false]; + } + + for (let requireUserInteraction of prefVals) { + Services.prefs.setBoolPref( + "browser.navigation.requireUserInteraction", + requireUserInteraction + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE + "?entry=0" + ); + + assertBackForwardState(false, false); + + await followLink(TEST_PAGE + "?entry=1"); + + assertBackForwardState(true, false); + + await followLink(TEST_PAGE + "?entry=2"); + + assertBackForwardState(true, false); + + await followLink(TEST_PAGE + "?entry=3"); + + assertBackForwardState(true, false); + + // Entry 4 will be added through a user action in browser chrome, + // giving user interaction to entry 3. Entry 4 should not gain automatic + // user interaction. + await privilegedLoad(TEST_PAGE + "?entry=4"); + + assertBackForwardState(true, false); + + await followLink(TEST_PAGE + "?entry=5"); + + assertBackForwardState(true, false); + + if (!requireUserInteraction) { + await goBack(TEST_PAGE + "?entry=4"); + } + await goBack(TEST_PAGE + "?entry=3"); + + if (!requireUserInteraction) { + await goBack(TEST_PAGE + "?entry=2"); + await goBack(TEST_PAGE + "?entry=1"); + } + + assertBackForwardState(true, true); + + await goBack(TEST_PAGE + "?entry=0"); + + assertBackForwardState(false, true); + + if (!requireUserInteraction) { + await goForward(TEST_PAGE + "?entry=1"); + await goForward(TEST_PAGE + "?entry=2"); + } + + await goForward(TEST_PAGE + "?entry=3"); + + assertBackForwardState(true, true); + + if (!requireUserInteraction) { + await goForward(TEST_PAGE + "?entry=4"); + } + + await goForward(TEST_PAGE + "?entry=5"); + + assertBackForwardState(true, false); + + BrowserTestUtils.removeTab(tab); + } + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); +} + +// Test that we add a user interaction flag to the previous site when loading +// a new site from user interaction with privileged UI, e.g. through the +// URL bar. +add_task(async function test_urlBar() { + await runTest(async function (url) { + info(`Loading ${url} via the URL bar.`); + let browser = gBrowser.selectedBrowser; + let loaded = BrowserTestUtils.browserLoaded(browser, false, url); + gURLBar.focus(); + gURLBar.value = url; + gURLBar.goButton.click(); + await loaded; + }); +}); diff --git a/docshell/test/browser/browser_badCertDomainFixup.js b/docshell/test/browser/browser_badCertDomainFixup.js new file mode 100644 index 0000000000..2db3eb6701 --- /dev/null +++ b/docshell/test/browser/browser_badCertDomainFixup.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test checks if we are correctly fixing https URLs by prefixing +// with www. when we encounter a SSL_ERROR_BAD_CERT_DOMAIN error. +// For example, https://example.com -> https://www.example.com. + +const PREF_BAD_CERT_DOMAIN_FIX_ENABLED = + "security.bad_cert_domain_error.url_fix_enabled"; +const PREF_ALLOW_HIJACKING_LOCALHOST = + "network.proxy.allow_hijacking_localhost"; + +const BAD_CERT_DOMAIN_ERROR_URL = "https://badcertdomain.example.com:443"; +const FIXED_URL = "https://www.badcertdomain.example.com/"; + +const BAD_CERT_DOMAIN_ERROR_URL2 = + "https://mismatch.badcertdomain.example.com:443"; +const IPV4_ADDRESS = "https://127.0.0.3:433"; +const BAD_CERT_DOMAIN_ERROR_PORT = "https://badcertdomain.example.com:82"; + +async function verifyErrorPage(errorPageURL) { + let certErrorLoaded = BrowserTestUtils.waitForErrorPage( + gBrowser.selectedBrowser + ); + BrowserTestUtils.loadURIString(gBrowser, errorPageURL); + await certErrorLoaded; + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + let ec; + await ContentTaskUtils.waitForCondition(() => { + ec = content.document.getElementById("errorCode"); + return ec.textContent; + }, "Error code has been set inside the advanced button panel"); + is( + ec.textContent, + "SSL_ERROR_BAD_CERT_DOMAIN", + "Correct error code is shown" + ); + }); +} + +// Test that "www." is prefixed to a https url when we encounter a bad cert domain +// error if the "www." form is included in the certificate's subjectAltNames. +add_task(async function prefixBadCertDomain() { + // Turn off the pref and ensure that we show the error page as expected. + Services.prefs.setBoolPref(PREF_BAD_CERT_DOMAIN_FIX_ENABLED, false); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + await verifyErrorPage(BAD_CERT_DOMAIN_ERROR_URL); + info("Cert error is shown as expected when the fixup pref is disabled"); + + // Turn on the pref and test that we fix the HTTPS URL. + Services.prefs.setBoolPref(PREF_BAD_CERT_DOMAIN_FIX_ENABLED, true); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let loadSuccessful = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + FIXED_URL + ); + BrowserTestUtils.loadURIString(gBrowser, BAD_CERT_DOMAIN_ERROR_URL); + await loadSuccessful; + + info("The URL was fixed as expected"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test that we don't prefix "www." to a https url when we encounter a bad cert domain +// error under certain conditions. +add_task(async function ignoreBadCertDomain() { + Services.prefs.setBoolPref(PREF_BAD_CERT_DOMAIN_FIX_ENABLED, true); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + // Test for when "www." form is not present in the certificate. + await verifyErrorPage(BAD_CERT_DOMAIN_ERROR_URL2); + info("Certificate error was shown as expected"); + + // Test that urls with IP addresses are not fixed. + Services.prefs.setBoolPref(PREF_ALLOW_HIJACKING_LOCALHOST, true); + await verifyErrorPage(IPV4_ADDRESS); + Services.prefs.clearUserPref(PREF_ALLOW_HIJACKING_LOCALHOST); + info("Certificate error was shown as expected for an IP address"); + + // Test that urls with ports are not fixed. + await verifyErrorPage(BAD_CERT_DOMAIN_ERROR_PORT); + info("Certificate error was shown as expected for a host with port"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/docshell/test/browser/browser_bfcache_copycommand.js b/docshell/test/browser/browser_bfcache_copycommand.js new file mode 100644 index 0000000000..178aee4913 --- /dev/null +++ b/docshell/test/browser/browser_bfcache_copycommand.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function dummyPageURL(domain, query = "") { + return ( + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + `https://${domain}` + ) + + "dummy_page.html" + + query + ); +} + +async function openContextMenu(browser) { + let contextMenu = document.getElementById("contentAreaContextMenu"); + let awaitPopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouse( + "body", + 1, + 1, + { + type: "contextmenu", + button: 2, + }, + browser + ); + await awaitPopupShown; +} + +async function closeContextMenu() { + let contextMenu = document.getElementById("contentAreaContextMenu"); + let awaitPopupHidden = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await awaitPopupHidden; +} + +async function testWithDomain(domain) { + // Passing a query to make sure the next load is never a same-document + // navigation. + let dummy = dummyPageURL("example.org", "?start"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, dummy); + let browser = tab.linkedBrowser; + + let sel = await SpecialPowers.spawn(browser, [], function () { + let sel = content.getSelection(); + sel.removeAllRanges(); + sel.selectAllChildren(content.document.body); + return sel.toString(); + }); + + await openContextMenu(browser); + + let copyItem = document.getElementById("context-copy"); + ok(!copyItem.disabled, "Copy item should be enabled if text is selected."); + + await closeContextMenu(); + + let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + BrowserTestUtils.loadURIString(browser, dummyPageURL(domain)); + await loaded; + loaded = BrowserTestUtils.waitForLocationChange(gBrowser, dummy); + browser.goBack(); + await loaded; + + let sel2 = await SpecialPowers.spawn(browser, [], function () { + return content.getSelection().toString(); + }); + is(sel, sel2, "Selection should remain when coming out of BFCache."); + + await openContextMenu(browser); + + ok(!copyItem.disabled, "Copy item should be enabled if text is selected."); + + await closeContextMenu(); + + await BrowserTestUtils.removeTab(tab); +} + +add_task(async function testValidSameOrigin() { + await testWithDomain("example.org"); +}); + +add_task(async function testValidCrossOrigin() { + await testWithDomain("example.com"); +}); + +add_task(async function testInvalid() { + await testWithDomain("this.is.invalid"); +}); diff --git a/docshell/test/browser/browser_browsingContext-01.js b/docshell/test/browser/browser_browsingContext-01.js new file mode 100644 index 0000000000..95831d2567 --- /dev/null +++ b/docshell/test/browser/browser_browsingContext-01.js @@ -0,0 +1,180 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = "about:blank"; + +async function getBrowsingContextId(browser, id) { + return SpecialPowers.spawn(browser, [id], async function (id) { + let contextId = content.window.docShell.browsingContext.id; + + let frames = [content.window]; + while (frames.length) { + let frame = frames.pop(); + let target = frame.document.getElementById(id); + if (target) { + contextId = target.docShell.browsingContext.id; + break; + } + + frames = frames.concat(Array.from(frame.frames)); + } + + return contextId; + }); +} + +async function addFrame(browser, id, parentId) { + return SpecialPowers.spawn( + browser, + [{ parentId, id }], + async function ({ parentId, id }) { + let parent = null; + if (parentId) { + let frames = [content.window]; + while (frames.length) { + let frame = frames.pop(); + let target = frame.document.getElementById(parentId); + if (target) { + parent = target.contentWindow.document.body; + break; + } + frames = frames.concat(Array.from(frame.frames)); + } + } else { + parent = content.document.body; + } + + let frame = await new Promise(resolve => { + let frame = content.document.createElement("iframe"); + frame.id = id || ""; + frame.url = "about:blank"; + frame.onload = () => resolve(frame); + parent.appendChild(frame); + }); + + return frame.contentWindow.docShell.browsingContext.id; + } + ); +} + +async function removeFrame(browser, id) { + return SpecialPowers.spawn(browser, [id], async function (id) { + let frames = [content.window]; + while (frames.length) { + let frame = frames.pop(); + let target = frame.document.getElementById(id); + if (target) { + target.remove(); + break; + } + + frames = frames.concat(Array.from(frame.frames)); + } + }); +} + +function getBrowsingContextById(id) { + return BrowsingContext.get(id); +} + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + let topId = await getBrowsingContextId(browser, ""); + let topContext = getBrowsingContextById(topId); + isnot(topContext, null); + is(topContext.parent, null); + is( + topId, + browser.browsingContext.id, + "<browser> has the correct browsingContext" + ); + is( + browser.browserId, + topContext.browserId, + "browsing context should have a correct <browser> id" + ); + + let id0 = await addFrame(browser, "frame0"); + let browsingContext0 = getBrowsingContextById(id0); + isnot(browsingContext0, null); + is(browsingContext0.parent, topContext); + + await removeFrame(browser, "frame0"); + + is(topContext.children.indexOf(browsingContext0), -1); + + // TODO(farre): Handle browsingContext removal [see Bug 1486719]. + todo_isnot(browsingContext0.parent, topContext); + } + ); +}); + +add_task(async function () { + // If Fission is disabled, the pref is no-op. + await SpecialPowers.pushPrefEnv({ set: [["fission.bfcacheInParent", true]] }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + ) + "dummy_page.html", + }, + async function (browser) { + let path = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + ); + await SpecialPowers.spawn(browser, [path], async function (path) { + var bc = new content.BroadcastChannel("browser_browsingContext"); + function waitForMessage(command) { + let p = new Promise(resolve => { + bc.addEventListener("message", e => resolve(e), { once: true }); + }); + command(); + return p; + } + + // Open a new window and wait for the message. + let e1 = await waitForMessage(_ => + content.window.open(path + "onpageshow_message.html", "", "noopener") + ); + + is(e1.data, "pageshow", "Got page show"); + + let e2 = await waitForMessage(_ => bc.postMessage("createiframe")); + is(e2.data.framesLength, 1, "Here we should have an iframe"); + + let e3 = await waitForMessage(_ => bc.postMessage("nextpage")); + + is(e3.data.event, "load"); + is(e3.data.framesLength, 0, "Here there shouldn't be an iframe"); + + // Return to the previous document. N.B. we expect to trigger + // BFCache here, hence we wait for pageshow. + let e4 = await waitForMessage(_ => bc.postMessage("back")); + + is(e4.data, "pageshow"); + + let e5 = await waitForMessage(_ => bc.postMessage("queryframes")); + is(e5.data.framesLength, 1, "And again there should be an iframe"); + + is(e5.outerWindowId, e2.outerWindowId, "BF cache cached outer window"); + is(e5.browsingContextId, e2.browsingContextId, "BF cache cached BC"); + + let e6 = await waitForMessage(_ => bc.postMessage("close")); + is(e6.data, "closed"); + + bc.close(); + }); + } + ); +}); diff --git a/docshell/test/browser/browser_browsingContext-02.js b/docshell/test/browser/browser_browsingContext-02.js new file mode 100644 index 0000000000..8439b869b0 --- /dev/null +++ b/docshell/test/browser/browser_browsingContext-02.js @@ -0,0 +1,235 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function (browser) { + const BASE1 = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + ); + const BASE2 = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://test1.example.com" + ); + const URL = BASE1 + "onload_message.html"; + let sixth = BrowserTestUtils.waitForNewTab( + gBrowser, + URL + "#sixth", + true, + true + ); + let seventh = BrowserTestUtils.waitForNewTab( + gBrowser, + URL + "#seventh", + true, + true + ); + let browserIds = await SpecialPowers.spawn( + browser, + [{ base1: BASE1, base2: BASE2 }], + async function ({ base1, base2 }) { + let top = content; + top.name = "top"; + top.location.href += "#top"; + + let contexts = { + top: top.location.href, + first: base1 + "dummy_page.html#first", + third: base2 + "dummy_page.html#third", + second: base1 + "dummy_page.html#second", + fourth: base2 + "dummy_page.html#fourth", + fifth: base1 + "dummy_page.html#fifth", + sixth: base1 + "onload_message.html#sixth", + seventh: base1 + "onload_message.html#seventh", + }; + + function addFrame(target, name) { + return content.SpecialPowers.spawn( + target, + [name, contexts[name]], + async (name, context) => { + let doc = this.content.document; + + let frame = doc.createElement("iframe"); + doc.body.appendChild(frame); + frame.name = name; + frame.src = context; + await new Promise(resolve => { + frame.addEventListener("load", resolve, { once: true }); + }); + return frame.browsingContext; + } + ); + } + + function addWindow(target, name, { options, resolve }) { + return content.SpecialPowers.spawn( + target, + [name, contexts[name], options, resolve], + (name, context, options, resolve) => { + let win = this.content.open(context, name, options); + let bc = win && win.docShell.browsingContext; + + if (resolve) { + return new Promise(resolve => + this.content.addEventListener("message", () => resolve(bc)) + ); + } + return Promise.resolve({ name }); + } + ); + } + + // We're going to create a tree that looks like the + // following. + // + // top sixth seventh + // / \ + // / \ / + // first second + // / \ / + // / \ + // third fourth - - - + // / + // / + // fifth + // + // The idea is to have one top level non-auxiliary browsing + // context, five nested, one top level auxiliary with an + // opener, and one top level without an opener. Given that + // set of related and one unrelated browsing contexts we + // wish to confirm that targeting is able to find + // appropriate browsing contexts. + + // WindowGlobalChild.findBrowsingContextWithName requires access + // checks, which can only be performed in the process of the accessor + // WindowGlobalChild. + function findWithName(bc, name) { + return content.SpecialPowers.spawn(bc, [name], name => { + return content.windowGlobalChild.findBrowsingContextWithName( + name + ); + }); + } + + async function reachable(start, target) { + info(start.name, target.name); + is( + await findWithName(start, target.name), + target, + [start.name, "can reach", target.name].join(" ") + ); + } + + async function unreachable(start, target) { + is( + await findWithName(start, target.name), + null, + [start.name, "can't reach", target.name].join(" ") + ); + } + + let first = await addFrame(top, "first"); + info("first"); + let second = await addFrame(top, "second"); + info("second"); + let third = await addFrame(first, "third"); + info("third"); + let fourth = await addFrame(first, "fourth"); + info("fourth"); + let fifth = await addFrame(fourth, "fifth"); + info("fifth"); + let sixth = await addWindow(fourth, "sixth", { resolve: true }); + info("sixth"); + let seventh = await addWindow(fourth, "seventh", { + options: ["noopener"], + }); + info("seventh"); + + let origin1 = [first, second, fifth, sixth]; + let origin2 = [third, fourth]; + + let topBC = BrowsingContext.getFromWindow(top); + let frames = new Map([ + [topBC, [topBC, first, second, third, fourth, fifth, sixth]], + [first, [topBC, ...origin1, third, fourth]], + [second, [topBC, ...origin1, third, fourth]], + [third, [topBC, ...origin2, fifth, sixth]], + [fourth, [topBC, ...origin2, fifth, sixth]], + [fifth, [topBC, ...origin1, third, fourth]], + [sixth, [...origin1, third, fourth]], + ]); + + for (let [start, accessible] of frames) { + for (let frame of frames.keys()) { + if (accessible.includes(frame)) { + await reachable(start, frame); + } else { + await unreachable(start, frame); + } + } + await unreachable(start, seventh); + } + + let topBrowserId = topBC.browserId; + ok(topBrowserId > 0, "Should have a browser ID."); + for (let [name, bc] of Object.entries({ + first, + second, + third, + fourth, + fifth, + })) { + is( + bc.browserId, + topBrowserId, + `${name} frame should have the same browserId as top.` + ); + } + + ok(sixth.browserId > 0, "sixth should have a browserId."); + isnot( + sixth.browserId, + topBrowserId, + "sixth frame should have a different browserId to top." + ); + + return [topBrowserId, sixth.browserId]; + } + ); + + [sixth, seventh] = await Promise.all([sixth, seventh]); + + is( + browser.browserId, + browserIds[0], + "browser should have the right browserId." + ); + is( + browser.browsingContext.browserId, + browserIds[0], + "browser's BrowsingContext should have the right browserId." + ); + is( + sixth.linkedBrowser.browserId, + browserIds[1], + "sixth should have the right browserId." + ); + is( + sixth.linkedBrowser.browsingContext.browserId, + browserIds[1], + "sixth's BrowsingContext should have the right browserId." + ); + + for (let tab of [sixth, seventh]) { + BrowserTestUtils.removeTab(tab); + } + } + ); +}); diff --git a/docshell/test/browser/browser_browsingContext-embedder.js b/docshell/test/browser/browser_browsingContext-embedder.js new file mode 100644 index 0000000000..9473a46eb4 --- /dev/null +++ b/docshell/test/browser/browser_browsingContext-embedder.js @@ -0,0 +1,156 @@ +"use strict"; + +function observeOnce(topic) { + return new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (topic == aTopic) { + Services.obs.removeObserver(observer, topic); + setTimeout(() => resolve(aSubject), 0); + } + }, topic); + }); +} + +add_task(async function runTest() { + let fissionWindow = await BrowserTestUtils.openNewBrowserWindow({ + fission: true, + remote: true, + }); + + info(`chrome, parent`); + let chromeBC = fissionWindow.docShell.browsingContext; + ok(chromeBC.currentWindowGlobal, "Should have a current WindowGlobal"); + is(chromeBC.embedderWindowGlobal, null, "chrome has no embedder global"); + is(chromeBC.embedderElement, null, "chrome has no embedder element"); + is(chromeBC.parent, null, "chrome has no parent"); + + // Open a new tab, and check that basic frames work out. + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser: fissionWindow.gBrowser, + }); + + info(`root, parent`); + let rootBC = tab.linkedBrowser.browsingContext; + ok(rootBC.currentWindowGlobal, "[parent] root has a window global"); + is( + rootBC.embedderWindowGlobal, + chromeBC.currentWindowGlobal, + "[parent] root has chrome as embedder global" + ); + is( + rootBC.embedderElement, + tab.linkedBrowser, + "[parent] root has browser as embedder element" + ); + is(rootBC.parent, null, "[parent] root has no parent"); + + // Test with an in-process frame + let frameId = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + info(`root, child`); + let rootBC = content.docShell.browsingContext; + is(rootBC.embedderElement, null, "[child] root has no embedder"); + is(rootBC.parent, null, "[child] root has no parent"); + + info(`frame, child`); + let iframe = content.document.createElement("iframe"); + content.document.body.appendChild(iframe); + + let frameBC = iframe.contentWindow.docShell.browsingContext; + is(frameBC.embedderElement, iframe, "[child] frame embedded within iframe"); + is(frameBC.parent, rootBC, "[child] frame has root as parent"); + + return frameBC.id; + }); + + info(`frame, parent`); + let frameBC = BrowsingContext.get(frameId); + ok(frameBC.currentWindowGlobal, "[parent] frame has a window global"); + is( + frameBC.embedderWindowGlobal, + rootBC.currentWindowGlobal, + "[parent] frame has root as embedder global" + ); + is(frameBC.embedderElement, null, "[parent] frame has no embedder element"); + is(frameBC.parent, rootBC, "[parent] frame has root as parent"); + + // Test with an out-of-process iframe. + + let oopID = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + info(`creating oop iframe`); + let oop = content.document.createElement("iframe"); + oop.setAttribute("src", "https://example.com"); + content.document.body.appendChild(oop); + + await new Promise(resolve => { + oop.addEventListener("load", resolve, { once: true }); + }); + + info(`oop frame, child`); + let oopBC = oop.frameLoader.browsingContext; + is(oopBC.embedderElement, oop, "[child] oop frame embedded within iframe"); + is( + oopBC.parent, + content.docShell.browsingContext, + "[child] frame has root as parent" + ); + + return oopBC.id; + }); + + info(`oop frame, parent`); + let oopBC = BrowsingContext.get(oopID); + is( + oopBC.embedderWindowGlobal, + rootBC.currentWindowGlobal, + "[parent] oop frame has root as embedder global" + ); + is(oopBC.embedderElement, null, "[parent] oop frame has no embedder element"); + is(oopBC.parent, rootBC, "[parent] oop frame has root as parent"); + + info(`waiting for oop window global`); + ok(oopBC.currentWindowGlobal, "[parent] oop frame has a window global"); + + // Open a new window, and adopt |tab| into it. + + let newWindow = await BrowserTestUtils.openNewBrowserWindow({ + fission: true, + remote: true, + }); + + info(`new chrome, parent`); + let newChromeBC = newWindow.docShell.browsingContext; + ok(newChromeBC.currentWindowGlobal, "Should have a current WindowGlobal"); + is( + newChromeBC.embedderWindowGlobal, + null, + "new chrome has no embedder global" + ); + is(newChromeBC.embedderElement, null, "new chrome has no embedder element"); + is(newChromeBC.parent, null, "new chrome has no parent"); + + isnot(newChromeBC, chromeBC, "different chrome browsing context"); + + info(`adopting tab`); + let newTab = newWindow.gBrowser.adoptTab(tab); + + is( + newTab.linkedBrowser.browsingContext, + rootBC, + "[parent] root browsing context survived" + ); + is( + rootBC.embedderWindowGlobal, + newChromeBC.currentWindowGlobal, + "[parent] embedder window global updated" + ); + is( + rootBC.embedderElement, + newTab.linkedBrowser, + "[parent] embedder element updated" + ); + is(rootBC.parent, null, "[parent] root has no parent"); + + info(`closing window`); + await BrowserTestUtils.closeWindow(newWindow); + await BrowserTestUtils.closeWindow(fissionWindow); +}); diff --git a/docshell/test/browser/browser_browsingContext-getAllBrowsingContextsInSubtree.js b/docshell/test/browser/browser_browsingContext-getAllBrowsingContextsInSubtree.js new file mode 100644 index 0000000000..076383ad87 --- /dev/null +++ b/docshell/test/browser/browser_browsingContext-getAllBrowsingContextsInSubtree.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function addFrame(url) { + let iframe = content.document.createElement("iframe"); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + iframe.src = url; + content.document.body.appendChild(iframe); + }); + return iframe.browsingContext; +} + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async browser => { + // Add 15 example.com frames to the toplevel document. + let frames = await Promise.all( + Array.from({ length: 15 }).map(_ => + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + SpecialPowers.spawn(browser, ["http://example.com/"], addFrame) + ) + ); + + // Add an example.org subframe to each example.com frame. + let subframes = await Promise.all( + Array.from({ length: 15 }).map((_, i) => + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + SpecialPowers.spawn(frames[i], ["http://example.org/"], addFrame) + ) + ); + + Assert.deepEqual( + subframes[0].getAllBrowsingContextsInSubtree(), + [subframes[0]], + "Childless context only has self in subtree" + ); + Assert.deepEqual( + frames[0].getAllBrowsingContextsInSubtree(), + [frames[0], subframes[0]], + "Single-child context has 2 contexts in subtree" + ); + Assert.deepEqual( + browser.browsingContext.getAllBrowsingContextsInSubtree(), + [browser.browsingContext, ...frames, ...subframes], + "Toplevel context has all subtree contexts" + ); + } + ); +}); diff --git a/docshell/test/browser/browser_browsingContext-getWindowByName.js b/docshell/test/browser/browser_browsingContext-getWindowByName.js new file mode 100644 index 0000000000..e57691b270 --- /dev/null +++ b/docshell/test/browser/browser_browsingContext-getWindowByName.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function addWindow(name) { + var blank = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + blank.data = "about:blank"; + let promise = BrowserTestUtils.waitForNewWindow({ + anyWindow: true, + url: "about:blank", + }); + Services.ww.openWindow( + null, + AppConstants.BROWSER_CHROME_URL, + name, + "chrome,dialog=no", + blank + ); + return promise; +} + +add_task(async function () { + let windows = [await addWindow("first"), await addWindow("second")]; + + for (let w of windows) { + isnot(w, null); + is(Services.ww.getWindowByName(w.name, null), w, `Found ${w.name}`); + } + + await Promise.all(windows.map(BrowserTestUtils.closeWindow)); +}); diff --git a/docshell/test/browser/browser_browsingContext-webProgress.js b/docshell/test/browser/browser_browsingContext-webProgress.js new file mode 100644 index 0000000000..54b5400efd --- /dev/null +++ b/docshell/test/browser/browser_browsingContext-webProgress.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + const browser = tab.linkedBrowser; + const aboutBlankBrowsingContext = browser.browsingContext; + const { webProgress } = aboutBlankBrowsingContext; + ok( + webProgress, + "Got a WebProgress interface on BrowsingContext in the parent process" + ); + is( + webProgress.browsingContext, + browser.browsingContext, + "WebProgress.browsingContext refers to the right BrowsingContext" + ); + + const onLocationChanged = waitForNextLocationChange(webProgress); + const newLocation = "data:text/html;charset=utf-8,first-page"; + let loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, newLocation); + await loaded; + + const firstPageBrowsingContext = browser.browsingContext; + const isBfcacheInParentEnabled = + SpecialPowers.Services.appinfo.sessionHistoryInParent && + SpecialPowers.Services.prefs.getBoolPref("fission.bfcacheInParent"); + if (isBfcacheInParentEnabled) { + isnot( + aboutBlankBrowsingContext, + firstPageBrowsingContext, + "With bfcache in parent, navigations spawn a new BrowsingContext" + ); + } else { + is( + aboutBlankBrowsingContext, + firstPageBrowsingContext, + "Without bfcache in parent, navigations reuse the same BrowsingContext" + ); + } + + info("Wait for onLocationChange to be fired"); + { + const { browsingContext, location, request, flags } = + await onLocationChanged; + is( + browsingContext, + firstPageBrowsingContext, + "Location change fires on the new BrowsingContext" + ); + ok(location instanceof Ci.nsIURI); + is(location.spec, newLocation); + ok(request instanceof Ci.nsIChannel); + is(request.URI.spec, newLocation); + is(flags, 0); + } + + const onIframeLocationChanged = waitForNextLocationChange(webProgress); + const iframeLocation = "data:text/html;charset=utf-8,iframe"; + const iframeBC = await SpecialPowers.spawn( + browser, + [iframeLocation], + async url => { + const iframe = content.document.createElement("iframe"); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + iframe.src = url; + content.document.body.appendChild(iframe); + }); + + return iframe.browsingContext; + } + ); + ok( + iframeBC.webProgress, + "The iframe BrowsingContext also exposes a WebProgress" + ); + { + const { browsingContext, location, request, flags } = + await onIframeLocationChanged; + is( + browsingContext, + iframeBC, + "Iframe location change fires on the iframe BrowsingContext" + ); + ok(location instanceof Ci.nsIURI); + is(location.spec, iframeLocation); + ok(request instanceof Ci.nsIChannel); + is(request.URI.spec, iframeLocation); + is(flags, 0); + } + + const onSecondLocationChanged = waitForNextLocationChange(webProgress); + const onSecondPageDocumentStart = waitForNextDocumentStart(webProgress); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const secondLocation = "http://example.com/document-builder.sjs?html=com"; + loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, secondLocation); + await loaded; + + const secondPageBrowsingContext = browser.browsingContext; + if (isBfcacheInParentEnabled) { + isnot( + firstPageBrowsingContext, + secondPageBrowsingContext, + "With bfcache in parent, navigations spawn a new BrowsingContext" + ); + } else { + is( + firstPageBrowsingContext, + secondPageBrowsingContext, + "Without bfcache in parent, navigations reuse the same BrowsingContext" + ); + } + { + const { browsingContext, location, request, flags } = + await onSecondLocationChanged; + is( + browsingContext, + secondPageBrowsingContext, + "Second location change fires on the new BrowsingContext" + ); + ok(location instanceof Ci.nsIURI); + is(location.spec, secondLocation); + ok(request instanceof Ci.nsIChannel); + is(request.URI.spec, secondLocation); + is(flags, 0); + } + { + const { browsingContext, request } = await onSecondPageDocumentStart; + is( + browsingContext, + firstPageBrowsingContext, + "STATE_START, when navigating to another process, fires on the BrowsingContext we navigate *from*" + ); + ok(request instanceof Ci.nsIChannel); + is(request.URI.spec, secondLocation); + } + + const onBackLocationChanged = waitForNextLocationChange(webProgress, true); + const onBackDocumentStart = waitForNextDocumentStart(webProgress); + browser.goBack(); + + { + const { browsingContext, location, request, flags } = + await onBackLocationChanged; + is( + browsingContext, + firstPageBrowsingContext, + "location change, when navigating back, fires on the BrowsingContext we navigate *to*" + ); + ok(location instanceof Ci.nsIURI); + is(location.spec, newLocation); + ok(request instanceof Ci.nsIChannel); + is(request.URI.spec, newLocation); + is(flags, 0); + } + { + const { browsingContext, request } = await onBackDocumentStart; + is( + browsingContext, + secondPageBrowsingContext, + "STATE_START, when navigating back, fires on the BrowsingContext we navigate *from*" + ); + ok(request instanceof Ci.nsIChannel); + is(request.URI.spec, newLocation); + } + + BrowserTestUtils.removeTab(tab); +}); + +function waitForNextLocationChange(webProgress, onlyTopLevel = false) { + return new Promise(resolve => { + const wpl = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + onLocationChange(progress, request, location, flags) { + if (onlyTopLevel && progress.browsingContext.parent) { + // Ignore non-toplevel. + return; + } + webProgress.removeProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL); + resolve({ + browsingContext: progress.browsingContext, + location, + request, + flags, + }); + }, + }; + // Add a strong reference to the progress listener. + resolve.wpl = wpl; + webProgress.addProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL); + }); +} + +function waitForNextDocumentStart(webProgress) { + return new Promise(resolve => { + const wpl = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + onStateChange(progress, request, flags, status) { + if ( + flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT && + flags & Ci.nsIWebProgressListener.STATE_START + ) { + webProgress.removeProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL); + resolve({ browsingContext: progress.browsingContext, request }); + } + }, + }; + // Add a strong reference to the progress listener. + resolve.wpl = wpl; + webProgress.addProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL); + }); +} diff --git a/docshell/test/browser/browser_browsing_context_attached.js b/docshell/test/browser/browser_browsing_context_attached.js new file mode 100644 index 0000000000..60ef5e4aa6 --- /dev/null +++ b/docshell/test/browser/browser_browsing_context_attached.js @@ -0,0 +1,179 @@ +"use strict"; + +const TEST_PATH = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + ) + "dummy_page.html"; + +const TOPIC = "browsing-context-attached"; + +async function observeAttached(callback) { + let attached = new Map(); + + function observer(subject, topic, data) { + is(topic, TOPIC, "observing correct topic"); + ok(BrowsingContext.isInstance(subject), "subject to be a BrowsingContext"); + is(typeof data, "string", "data to be a String"); + info(`*** bc id=${subject.id}, why=${data}`); + attached.set(subject.id, { browsingContext: subject, why: data }); + } + + Services.obs.addObserver(observer, TOPIC); + try { + await callback(); + return attached; + } finally { + Services.obs.removeObserver(observer, TOPIC); + } +} + +add_task(async function toplevelForNewWindow() { + let win; + + let attached = await observeAttached(async () => { + win = await BrowserTestUtils.openNewBrowserWindow(); + }); + + ok( + attached.has(win.browsingContext.id), + "got notification for window's chrome browsing context" + ); + is( + attached.get(win.browsingContext.id).why, + "attach", + "expected reason for chrome browsing context" + ); + + ok( + attached.has(win.gBrowser.selectedBrowser.browsingContext.id), + "got notification for toplevel browsing context" + ); + is( + attached.get(win.gBrowser.selectedBrowser.browsingContext.id).why, + "attach", + "expected reason for toplevel browsing context" + ); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function toplevelForNewTab() { + let tab; + + let attached = await observeAttached(async () => { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + }); + + ok( + !attached.has(window.browsingContext.id), + "no notification for the current window's chrome browsing context" + ); + ok( + attached.has(tab.linkedBrowser.browsingContext.id), + "got notification for toplevel browsing context" + ); + is( + attached.get(tab.linkedBrowser.browsingContext.id).why, + "attach", + "expected reason for toplevel browsing context" + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function subframe() { + let browsingContext; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let attached = await observeAttached(async () => { + browsingContext = await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + let iframe = content.document.createElement("iframe"); + content.document.body.appendChild(iframe); + iframe.contentWindow.location = "https://example.com/"; + return iframe.browsingContext; + }); + }); + + ok( + !attached.has(window.browsingContext.id), + "no notification for the current window's chrome browsing context" + ); + ok( + !attached.has(tab.linkedBrowser.browsingContext.id), + "no notification for toplevel browsing context" + ); + ok( + attached.has(browsingContext.id), + "got notification for frame's browsing context" + ); + is( + attached.get(browsingContext.id).why, + "attach", + "expected reason for frame's browsing context" + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function toplevelReplacedBy() { + let tab; + + let attached = await observeAttached(async () => { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots"); + }); + + const firstContext = tab.linkedBrowser.browsingContext; + ok( + attached.has(firstContext.id), + "got notification for initial toplevel browsing context" + ); + is( + attached.get(firstContext.id).why, + "attach", + "expected reason for initial toplevel browsing context" + ); + + attached = await observeAttached(async () => { + await loadURI(TEST_PATH); + }); + const secondContext = tab.linkedBrowser.browsingContext; + ok( + attached.has(secondContext.id), + "got notification for replaced toplevel browsing context" + ); + isnot(secondContext, firstContext, "browsing context to be replaced"); + is( + attached.get(secondContext.id).why, + "replace", + "expected reason for replaced toplevel browsing context" + ); + is( + secondContext.browserId, + firstContext.browserId, + "browserId has been kept" + ); + + attached = await observeAttached(async () => { + await loadURI("about:robots"); + }); + const thirdContext = tab.linkedBrowser.browsingContext; + ok( + attached.has(thirdContext.id), + "got notification for replaced toplevel browsing context" + ); + isnot(thirdContext, secondContext, "browsing context to be replaced"); + is( + attached.get(thirdContext.id).why, + "replace", + "expected reason for replaced toplevel browsing context" + ); + is( + thirdContext.browserId, + secondContext.browserId, + "browserId has been kept" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/docshell/test/browser/browser_browsing_context_discarded.js b/docshell/test/browser/browser_browsing_context_discarded.js new file mode 100644 index 0000000000..a300737d4f --- /dev/null +++ b/docshell/test/browser/browser_browsing_context_discarded.js @@ -0,0 +1,65 @@ +"use strict"; + +const TOPIC = "browsing-context-discarded"; + +async function observeDiscarded(browsingContexts, callback) { + let discarded = []; + + let promise = BrowserUtils.promiseObserved(TOPIC, subject => { + ok(BrowsingContext.isInstance(subject), "subject to be a BrowsingContext"); + discarded.push(subject); + + return browsingContexts.every(item => discarded.includes(item)); + }); + await callback(); + await promise; + + return discarded; +} + +add_task(async function toplevelForNewWindow() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + let browsingContext = win.gBrowser.selectedBrowser.browsingContext; + + await observeDiscarded([win.browsingContext, browsingContext], async () => { + await BrowserTestUtils.closeWindow(win); + }); +}); + +add_task(async function toplevelForNewTab() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let browsingContext = tab.linkedBrowser.browsingContext; + + let discarded = await observeDiscarded([browsingContext], () => { + BrowserTestUtils.removeTab(tab); + }); + + ok( + !discarded.includes(window.browsingContext), + "no notification for the current window's chrome browsing context" + ); +}); + +add_task(async function subframe() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let browsingContext = await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + let iframe = content.document.createElement("iframe"); + content.document.body.appendChild(iframe); + iframe.contentWindow.location = "https://example.com/"; + return iframe.browsingContext; + }); + + let discarded = await observeDiscarded([browsingContext], async () => { + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + let iframe = content.document.querySelector("iframe"); + iframe.remove(); + }); + }); + + ok( + !discarded.includes(tab.browsingContext), + "no notification for toplevel browsing context" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/docshell/test/browser/browser_bug1206879.js b/docshell/test/browser/browser_bug1206879.js new file mode 100644 index 0000000000..38e17633b8 --- /dev/null +++ b/docshell/test/browser/browser_bug1206879.js @@ -0,0 +1,50 @@ +add_task(async function () { + let url = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/" + ) + "file_bug1206879.html"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url, true); + + let numLocationChanges = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + let webprogress = content.docShell.QueryInterface(Ci.nsIWebProgress); + let locationChangeCount = 0; + let listener = { + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + info("onLocationChange: " + aLocation.spec); + locationChangeCount++; + this.resolve(); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + }; + let locationPromise = new Promise((resolve, reject) => { + listener.resolve = resolve; + }); + webprogress.addProgressListener( + listener, + Ci.nsIWebProgress.NOTIFY_LOCATION + ); + + content.frames[0].history.pushState(null, null, "foo"); + + await locationPromise; + webprogress.removeProgressListener(listener); + + return locationChangeCount; + } + ); + + gBrowser.removeTab(tab); + is( + numLocationChanges, + 1, + "pushState with a different URI should cause a LocationChange event." + ); +}); diff --git a/docshell/test/browser/browser_bug1309900_crossProcessHistoryNavigation.js b/docshell/test/browser/browser_bug1309900_crossProcessHistoryNavigation.js new file mode 100644 index 0000000000..9b99698367 --- /dev/null +++ b/docshell/test/browser/browser_bug1309900_crossProcessHistoryNavigation.js @@ -0,0 +1,54 @@ +/* -*- 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/. */ + +add_task(async function runTests() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:about" + ); + + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + }); + + let browser = tab.linkedBrowser; + + let loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, "about:config"); + let href = await loaded; + is(href, "about:config", "Check about:config loaded"); + + // Using a dummy onunload listener to disable the bfcache as that can prevent + // the test browser load detection mechanism from working. + loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString( + browser, + "data:text/html,<body%20onunload=''><iframe></iframe></body>" + ); + href = await loaded; + is( + href, + "data:text/html,<body%20onunload=''><iframe></iframe></body>", + "Check data URL loaded" + ); + + loaded = BrowserTestUtils.browserLoaded(browser); + browser.goBack(); + href = await loaded; + is(href, "about:config", "Check we've gone back to about:config"); + + loaded = BrowserTestUtils.browserLoaded(browser); + browser.goForward(); + href = await loaded; + is( + href, + "data:text/html,<body%20onunload=''><iframe></iframe></body>", + "Check we've gone forward to data URL" + ); +}); diff --git a/docshell/test/browser/browser_bug1328501.js b/docshell/test/browser/browser_bug1328501.js new file mode 100644 index 0000000000..2be74b04d0 --- /dev/null +++ b/docshell/test/browser/browser_bug1328501.js @@ -0,0 +1,69 @@ +const HTML_URL = + "http://mochi.test:8888/browser/docshell/test/browser/file_bug1328501.html"; +const FRAME_URL = + "http://mochi.test:8888/browser/docshell/test/browser/file_bug1328501_frame.html"; +const FRAME_SCRIPT_URL = + "chrome://mochitests/content/browser/docshell/test/browser/file_bug1328501_framescript.js"; +add_task(async function testMultiFrameRestore() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.navigation.requireUserInteraction", false], + // Disable bfcache so that dummy_page.html doesn't enter there. + // The actual test page does already prevent bfcache and the test + // is just for http-on-opening-request handling in the child process. + ["browser.sessionhistory.max_total_viewers", 0], + ], + }); + await BrowserTestUtils.withNewTab( + { gBrowser, url: HTML_URL }, + async function (browser) { + // Navigate 2 subframes and load about:blank. + let browserLoaded = BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn( + browser, + [FRAME_URL], + async function (FRAME_URL) { + function frameLoaded(frame) { + frame.contentWindow.location = FRAME_URL; + return new Promise(r => (frame.onload = r)); + } + let frame1 = content.document.querySelector("#testFrame1"); + let frame2 = content.document.querySelector("#testFrame2"); + ok(frame1, "check found testFrame1"); + ok(frame2, "check found testFrame2"); + await frameLoaded(frame1); + await frameLoaded(frame2); + content.location = "dummy_page.html"; + } + ); + await browserLoaded; + + // Load a frame script to query nsIDOMWindow on "http-on-opening-request", + // which will force about:blank content viewers being created. + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + + // The frame script also forwards frames-loaded. + let framesLoaded = BrowserTestUtils.waitForMessage( + browser.messageManager, + "test:frames-loaded" + ); + + browser.goBack(); + await framesLoaded; + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 1000)); + await SpecialPowers.spawn(browser, [FRAME_URL], FRAME_URL => { + is( + content.document.querySelector("#testFrame1").contentWindow.location + .href, + FRAME_URL + ); + is( + content.document.querySelector("#testFrame2").contentWindow.location + .href, + FRAME_URL + ); + }); + } + ); +}); diff --git a/docshell/test/browser/browser_bug1347823.js b/docshell/test/browser/browser_bug1347823.js new file mode 100644 index 0000000000..53a968dc12 --- /dev/null +++ b/docshell/test/browser/browser_bug1347823.js @@ -0,0 +1,91 @@ +/** + * Test that session history's expiration tracker would remove bfcache on + * expiration. + */ + +// With bfcache not expired. +add_task(async function testValidCache() { + // Make an unrealistic large timeout. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.sessionhistory.contentViewerTimeout", 86400], + // If Fission is disabled, the pref is no-op. + ["fission.bfcacheInParent", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "data:text/html;charset=utf-8,pageA1" }, + async function (browser) { + // Make a simple modification for bfcache testing. + await SpecialPowers.spawn(browser, [], () => { + content.document.body.textContent = "modified"; + }); + + // Load a random page. + BrowserTestUtils.loadURIString( + browser, + "data:text/html;charset=utf-8,pageA2" + ); + await BrowserTestUtils.browserLoaded(browser); + + // Go back and verify text content. + let awaitPageShow = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goBack(); + await awaitPageShow; + await SpecialPowers.spawn(browser, [], () => { + is(content.document.body.textContent, "modified"); + }); + } + ); +}); + +// With bfcache expired. +add_task(async function testExpiredCache() { + // Make bfcache timeout in 1 sec. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.sessionhistory.contentViewerTimeout", 1], + // If Fission is disabled, the pref is no-op. + ["fission.bfcacheInParent", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "data:text/html;charset=utf-8,pageB1" }, + async function (browser) { + // Make a simple modification for bfcache testing. + await SpecialPowers.spawn(browser, [], () => { + content.document.body.textContent = "modified"; + }); + + // Load a random page. + BrowserTestUtils.loadURIString( + browser, + "data:text/html;charset=utf-8,pageB2" + ); + await BrowserTestUtils.browserLoaded(browser); + + // Wait for 3 times of expiration timeout, hopefully it's evicted... + await SpecialPowers.spawn(browser, [], () => { + return new Promise(resolve => { + content.setTimeout(resolve, 5000); + }); + }); + + // Go back and verify text content. + let awaitPageShow = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goBack(); + await awaitPageShow; + await SpecialPowers.spawn(browser, [], () => { + is(content.document.body.textContent, "pageB1"); + }); + } + ); +}); diff --git a/docshell/test/browser/browser_bug134911.js b/docshell/test/browser/browser_bug134911.js new file mode 100644 index 0000000000..9b52eeab47 --- /dev/null +++ b/docshell/test/browser/browser_bug134911.js @@ -0,0 +1,57 @@ +const TEXT = { + /* The test text decoded correctly as Shift_JIS */ + rightText: + "\u30E6\u30CB\u30B3\u30FC\u30C9\u306F\u3001\u3059\u3079\u3066\u306E\u6587\u5B57\u306B\u56FA\u6709\u306E\u756A\u53F7\u3092\u4ED8\u4E0E\u3057\u307E\u3059", + + enteredText1: "The quick brown fox jumps over the lazy dog", + enteredText2: + "\u03BE\u03B5\u03C3\u03BA\u03B5\u03C0\u03AC\u03B6\u03C9\u0020\u03C4\u1F74\u03BD\u0020\u03C8\u03C5\u03C7\u03BF\u03C6\u03B8\u03CC\u03C1\u03B1\u0020\u03B2\u03B4\u03B5\u03BB\u03C5\u03B3\u03BC\u03AF\u03B1", +}; + +function test() { + waitForExplicitFinish(); + + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + rootDir + "test-form_sjis.html" + ); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(afterOpen); +} + +function afterOpen() { + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then( + afterChangeCharset + ); + + SpecialPowers.spawn(gBrowser.selectedBrowser, [TEXT], function (TEXT) { + content.document.getElementById("testtextarea").value = TEXT.enteredText1; + content.document.getElementById("testinput").value = TEXT.enteredText2; + }).then(() => { + /* Force the page encoding to Shift_JIS */ + BrowserForceEncodingDetection(); + }); +} + +function afterChangeCharset() { + SpecialPowers.spawn(gBrowser.selectedBrowser, [TEXT], function (TEXT) { + is( + content.document.getElementById("testpar").innerHTML, + TEXT.rightText, + "encoding successfully changed" + ); + is( + content.document.getElementById("testtextarea").value, + TEXT.enteredText1, + "text preserved in <textarea>" + ); + is( + content.document.getElementById("testinput").value, + TEXT.enteredText2, + "text preserved in <input>" + ); + }).then(() => { + gBrowser.removeCurrentTab(); + finish(); + }); +} diff --git a/docshell/test/browser/browser_bug1415918_beforeunload_options.js b/docshell/test/browser/browser_bug1415918_beforeunload_options.js new file mode 100644 index 0000000000..4aec24cda4 --- /dev/null +++ b/docshell/test/browser/browser_bug1415918_beforeunload_options.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" +); + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +SimpleTest.requestFlakyTimeout("Needs to test a timeout"); + +function delay(msec) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + return new Promise(resolve => setTimeout(resolve, msec)); +} + +function allowNextNavigation(browser) { + return PromptTestUtils.handleNextPrompt( + browser, + { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" }, + { buttonNumClick: 0 } + ); +} + +function cancelNextNavigation(browser) { + return PromptTestUtils.handleNextPrompt( + browser, + { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" }, + { buttonNumClick: 1 } + ); +} + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); + + const permitUnloadTimeout = Services.prefs.getIntPref( + "dom.beforeunload_timeout_ms" + ); + + let url = TEST_PATH + "dummy_page.html"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser.browsingContext, [], () => { + content.addEventListener("beforeunload", event => { + event.preventDefault(); + }); + }); + + /* + * Check condition where beforeunload handlers request a prompt. + */ + + // Prompt is shown, user clicks OK. + + let promptShownPromise = allowNextNavigation(browser); + ok(browser.permitUnload().permitUnload, "permit unload should be true"); + await promptShownPromise; + + // Prompt is shown, user clicks CANCEL. + promptShownPromise = cancelNextNavigation(browser); + ok(!browser.permitUnload().permitUnload, "permit unload should be false"); + await promptShownPromise; + + // Prompt is not shown, don't permit unload. + let promptShown = false; + let shownCallback = () => { + promptShown = true; + }; + + browser.addEventListener("DOMWillOpenModalDialog", shownCallback); + ok( + !browser.permitUnload("dontUnload").permitUnload, + "permit unload should be false" + ); + ok(!promptShown, "prompt should not have been displayed"); + + // Prompt is not shown, permit unload. + promptShown = false; + ok( + browser.permitUnload("unload").permitUnload, + "permit unload should be true" + ); + ok(!promptShown, "prompt should not have been displayed"); + browser.removeEventListener("DOMWillOpenModalDialog", shownCallback); + + promptShownPromise = PromptTestUtils.waitForPrompt(browser, { + modalType: Services.prompt.MODAL_TYPE_CONTENT, + promptType: "confirmEx", + }); + + let promptDismissed = false; + let closedCallback = () => { + promptDismissed = true; + }; + + browser.addEventListener("DOMModalDialogClosed", closedCallback); + + let promise = browser.asyncPermitUnload(); + + let promiseResolved = false; + promise.then(() => { + promiseResolved = true; + }); + + let dialog = await promptShownPromise; + ok(!promiseResolved, "Should not have resolved promise yet"); + + await delay(permitUnloadTimeout * 1.5); + + ok(!promptDismissed, "Should not have dismissed prompt yet"); + ok(!promiseResolved, "Should not have resolved promise yet"); + + await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 }); + + let { permitUnload } = await promise; + ok(promptDismissed, "Should have dismissed prompt"); + ok(!permitUnload, "Should not have permitted unload"); + + browser.removeEventListener("DOMModalDialogClosed", closedCallback); + + promptShownPromise = allowNextNavigation(browser); + + /* + * Check condition where no one requests a prompt. In all cases, + * permitUnload should be true, and all handlers fired. + */ + url += "?1"; + BrowserTestUtils.loadURIString(browser, url); + await BrowserTestUtils.browserLoaded(browser, false, url); + await promptShownPromise; + + promptShown = false; + browser.addEventListener("DOMWillOpenModalDialog", shownCallback); + + ok(browser.permitUnload().permitUnload, "permit unload should be true"); + ok(!promptShown, "prompt should not have been displayed"); + + promptShown = false; + ok( + browser.permitUnload("dontUnload").permitUnload, + "permit unload should be true" + ); + ok(!promptShown, "prompt should not have been displayed"); + + promptShown = false; + ok( + browser.permitUnload("unload").permitUnload, + "permit unload should be true" + ); + ok(!promptShown, "prompt should not have been displayed"); + + browser.removeEventListener("DOMWillOpenModalDialog", shownCallback); + + await BrowserTestUtils.removeTab(tab, { skipPermitUnload: true }); +}); diff --git a/docshell/test/browser/browser_bug1543077-3.js b/docshell/test/browser/browser_bug1543077-3.js new file mode 100644 index 0000000000..7cef4aef10 --- /dev/null +++ b/docshell/test/browser/browser_bug1543077-3.js @@ -0,0 +1,46 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug1543077-3.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u3042"), + 136, + "Parent doc should be ISO-2022-JP initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u3042"), + 92, + "Child doc should be ISO-2022-JP initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u3042"), + 136, + "Parent doc should decode as ISO-2022-JP subsequently" + ); + is( + content.frames[0].document.documentElement.textContent.indexOf("\u3042"), + 92, + "Child doc should decode as ISO-2022-JP subsequently" + ); + + is( + content.document.characterSet, + "ISO-2022-JP", + "Parent doc should report ISO-2022-JP subsequently" + ); + is( + content.frames[0].document.characterSet, + "ISO-2022-JP", + "Child doc should report ISO-2022-JP subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug1594938.js b/docshell/test/browser/browser_bug1594938.js new file mode 100644 index 0000000000..569afe6901 --- /dev/null +++ b/docshell/test/browser/browser_bug1594938.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test for Bug 1594938 + * + * If a session history listener blocks reloads we shouldn't crash. + */ + +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "https://example.com/" }, + async function (browser) { + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await SpecialPowers.spawn(browser, [], async function () { + let history = this.content.docShell.QueryInterface( + Ci.nsIWebNavigation + ).sessionHistory; + + let testDone = {}; + testDone.promise = new Promise(resolve => { + testDone.resolve = resolve; + }); + + let listenerCalled = false; + let listener = { + OnHistoryNewEntry: aNewURI => {}, + OnHistoryReload: () => { + listenerCalled = true; + this.content.setTimeout(() => { + testDone.resolve(); + }); + return false; + }, + OnHistoryGotoIndex: () => {}, + OnHistoryPurge: () => {}, + OnHistoryReplaceEntry: () => {}, + + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + + history.legacySHistory.addSHistoryListener(listener); + + history.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); + await testDone.promise; + + Assert.ok(listenerCalled, "reloads were blocked"); + + history.legacySHistory.removeSHistoryListener(listener); + }); + + return; + } + + let history = browser.browsingContext.sessionHistory; + + let testDone = {}; + testDone.promise = new Promise(resolve => { + testDone.resolve = resolve; + }); + + let listenerCalled = false; + let listener = { + OnHistoryNewEntry: aNewURI => {}, + OnHistoryReload: () => { + listenerCalled = true; + setTimeout(() => { + testDone.resolve(); + }); + return false; + }, + OnHistoryGotoIndex: () => {}, + OnHistoryPurge: () => {}, + OnHistoryReplaceEntry: () => {}, + + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + + history.addSHistoryListener(listener); + + await SpecialPowers.spawn(browser, [], () => { + let history = this.content.docShell.QueryInterface( + Ci.nsIWebNavigation + ).sessionHistory; + history.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); + }); + await testDone.promise; + + Assert.ok(listenerCalled, "reloads were blocked"); + + history.removeSHistoryListener(listener); + } + ); +}); diff --git a/docshell/test/browser/browser_bug1622420.js b/docshell/test/browser/browser_bug1622420.js new file mode 100644 index 0000000000..81026611e4 --- /dev/null +++ b/docshell/test/browser/browser_bug1622420.js @@ -0,0 +1,31 @@ +const ACTOR = "Bug1622420"; + +add_task(async function test() { + let base = getRootDirectory(gTestPath).slice(0, -1); + ChromeUtils.registerWindowActor(ACTOR, { + allFrames: true, + child: { + esModuleURI: `${base}/Bug1622420Child.sys.mjs`, + }, + }); + + registerCleanupFunction(async () => { + gBrowser.removeTab(tab); + + ChromeUtils.unregisterWindowActor(ACTOR); + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/docshell/test/browser/file_bug1622420.html" + ); + let childBC = tab.linkedBrowser.browsingContext.children[0]; + let success = await childBC.currentWindowGlobal + .getActor(ACTOR) + .sendQuery("hasWindowContextForTopBC"); + ok( + success, + "Should have a WindowContext for the top BrowsingContext in the process of a child BrowsingContext" + ); +}); diff --git a/docshell/test/browser/browser_bug1648464-1.js b/docshell/test/browser/browser_bug1648464-1.js new file mode 100644 index 0000000000..c2a8093a3d --- /dev/null +++ b/docshell/test/browser/browser_bug1648464-1.js @@ -0,0 +1,46 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug1648464-1.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u00A4"), + 146, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u00A4"), + 95, + "Child doc should be windows-1252 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u3042"), + 146, + "Parent doc should decode as EUC-JP subsequently" + ); + is( + content.frames[0].document.documentElement.textContent.indexOf("\u3042"), + 95, + "Child doc should decode as EUC-JP subsequently" + ); + + is( + content.document.characterSet, + "EUC-JP", + "Parent doc should report EUC-JP subsequently" + ); + is( + content.frames[0].document.characterSet, + "EUC-JP", + "Child doc should report EUC-JP subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug1673702.js b/docshell/test/browser/browser_bug1673702.js new file mode 100644 index 0000000000..02c48191b8 --- /dev/null +++ b/docshell/test/browser/browser_bug1673702.js @@ -0,0 +1,27 @@ +const DUMMY = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/docshell/test/browser/dummy_page.html"; +const JSON = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/browser/docshell/test/browser/file_bug1673702.json"; + +add_task(async function test_backAndReload() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: DUMMY }, + async function (browser) { + info("Start JSON load."); + BrowserTestUtils.loadURIString(browser, JSON); + await BrowserTestUtils.waitForLocationChange(gBrowser, JSON); + + info("JSON load has started, go back."); + browser.goBack(); + await BrowserTestUtils.browserStopped(browser); + + info("Reload."); + BrowserReload(); + await BrowserTestUtils.waitForLocationChange(gBrowser); + + is(browser.documentURI.spec, DUMMY); + } + ); +}); diff --git a/docshell/test/browser/browser_bug1674464.js b/docshell/test/browser/browser_bug1674464.js new file mode 100644 index 0000000000..4078cfe6c7 --- /dev/null +++ b/docshell/test/browser/browser_bug1674464.js @@ -0,0 +1,38 @@ +const DUMMY_1 = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/docshell/test/browser/dummy_page.html"; +const DUMMY_2 = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/browser/docshell/test/browser/dummy_page.html"; + +add_task(async function test_backAndReload() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: DUMMY_1 }, + async function (browser) { + await BrowserTestUtils.crashFrame(browser); + + info("Start second load."); + BrowserTestUtils.loadURIString(browser, DUMMY_2); + await BrowserTestUtils.waitForLocationChange(gBrowser, DUMMY_2); + + browser.goBack(); + await BrowserTestUtils.waitForLocationChange(gBrowser); + + is( + browser.browsingContext.childSessionHistory.index, + 0, + "We should have gone back to the first page" + ); + is( + browser.browsingContext.childSessionHistory.count, + 2, + "If a tab crashes after a load has finished we shouldn't have an entry for about:tabcrashed" + ); + is( + browser.documentURI.spec, + DUMMY_1, + "If a tab crashes after a load has finished we shouldn't have an entry for about:tabcrashed" + ); + } + ); +}); diff --git a/docshell/test/browser/browser_bug1688368-1.js b/docshell/test/browser/browser_bug1688368-1.js new file mode 100644 index 0000000000..04fc3dd9a8 --- /dev/null +++ b/docshell/test/browser/browser_bug1688368-1.js @@ -0,0 +1,24 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug1688368-1.sjs", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.body.textContent.indexOf("â"), + 0, + "Doc should be windows-1252 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.body.textContent.indexOf("☃"), + 0, + "Doc should be UTF-8 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug1691153.js b/docshell/test/browser/browser_bug1691153.js new file mode 100644 index 0000000000..70d7f91856 --- /dev/null +++ b/docshell/test/browser/browser_bug1691153.js @@ -0,0 +1,73 @@ +"use strict"; + +add_task(async () => { + const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + ); + + const HTML_URI = TEST_PATH + "file_bug1691153.html"; + + // Opening the page that contains the iframe + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let browser = tab.linkedBrowser; + let browserLoaded = BrowserTestUtils.browserLoaded( + browser, + true, + HTML_URI, + true + ); + info("new tab loaded"); + + BrowserTestUtils.loadURIString(browser, HTML_URI); + await browserLoaded; + info("The test page has loaded!"); + + let first_message_promise = SpecialPowers.spawn( + browser, + [], + async function () { + let blobPromise = new Promise((resolve, reject) => { + content.addEventListener("message", event => { + if (event.data.bloburl) { + info("Sanity check: recvd blob URL as " + event.data.bloburl); + resolve(event.data.bloburl); + } + }); + }); + content.postMessage("getblob", "*"); + return blobPromise; + } + ); + info("The test page has loaded!"); + let blob_url = await first_message_promise; + + Assert.ok(blob_url.startsWith("blob:"), "Sanity check: recvd blob"); + info(`Received blob URL message from content: ${blob_url}`); + // try to open the blob in a new tab, manually created by the user + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + blob_url, + true, + false, + true + ); + + let principal = gBrowser.selectedTab.linkedBrowser._contentPrincipal; + Assert.ok( + !principal.isSystemPrincipal, + "Newly opened blob shouldn't be Systemprincipal" + ); + Assert.ok( + !principal.isExpandedPrincipal, + "Newly opened blob shouldn't be ExpandedPrincipal" + ); + Assert.ok( + principal.isContentPrincipal, + "Newly opened blob tab should be ContentPrincipal" + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/docshell/test/browser/browser_bug1705872.js b/docshell/test/browser/browser_bug1705872.js new file mode 100644 index 0000000000..5c92228694 --- /dev/null +++ b/docshell/test/browser/browser_bug1705872.js @@ -0,0 +1,74 @@ +"use strict"; + +async function doLoadAndGoBack(browser, ext) { + let loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, "https://example.com/"); + await ext.awaitMessage("redir-handled"); + await loaded; + + let pageShownPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow", + true + ); + await SpecialPowers.spawn(browser, [], () => { + content.history.back(); + }); + return pageShownPromise; +} + +add_task(async function test_back() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "https://example.com/"], + web_accessible_resources: ["test.html"], + }, + files: { + "test.html": + "<!DOCTYPE html><html><head><title>Test add-on</title></head><body></body></html>", + }, + background: () => { + let { browser } = this; + browser.webRequest.onHeadersReceived.addListener( + details => { + if (details.statusCode != 200) { + return undefined; + } + browser.test.sendMessage("redir-handled"); + return { redirectUrl: browser.runtime.getURL("test.html") }; + }, + { + urls: ["https://example.com/"], + types: ["main_frame"], + }, + ["blocking"] + ); + }, + }); + + await extension.startup(); + + await BrowserTestUtils.withNewTab("about:home", async function (browser) { + await doLoadAndGoBack(browser, extension); + + await SpecialPowers.spawn(browser, [], () => { + is( + content.document.documentURI, + "about:home", + "Gone back to the right page" + ); + }); + + await doLoadAndGoBack(browser, extension); + + await SpecialPowers.spawn(browser, [], () => { + is( + content.document.documentURI, + "about:home", + "Gone back to the right page" + ); + }); + }); + + await extension.unload(); +}); diff --git a/docshell/test/browser/browser_bug1716290-1.js b/docshell/test/browser/browser_bug1716290-1.js new file mode 100644 index 0000000000..48add6f0eb --- /dev/null +++ b/docshell/test/browser/browser_bug1716290-1.js @@ -0,0 +1,24 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug1716290-1.sjs", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.characterSet, + "Shift_JIS", + "Doc should report Shift_JIS initially" + ); +} + +function afterChangeCharset() { + is( + content.document.characterSet, + "windows-1252", + "Doc should report windows-1252 subsequently (detector should override header)" + ); +} diff --git a/docshell/test/browser/browser_bug1716290-2.js b/docshell/test/browser/browser_bug1716290-2.js new file mode 100644 index 0000000000..a33c61f076 --- /dev/null +++ b/docshell/test/browser/browser_bug1716290-2.js @@ -0,0 +1,24 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug1716290-2.sjs", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.characterSet, + "Shift_JIS", + "Doc should report Shift_JIS initially" + ); +} + +function afterChangeCharset() { + is( + content.document.characterSet, + "windows-1252", + "Doc should report windows-1252 subsequently (detector should override meta resolving to the replacement encoding)" + ); +} diff --git a/docshell/test/browser/browser_bug1716290-3.js b/docshell/test/browser/browser_bug1716290-3.js new file mode 100644 index 0000000000..f7e6ca5bbd --- /dev/null +++ b/docshell/test/browser/browser_bug1716290-3.js @@ -0,0 +1,24 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug1716290-3.sjs", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.characterSet, + "Shift_JIS", + "Doc should report Shift_JIS initially" + ); +} + +function afterChangeCharset() { + is( + content.document.characterSet, + "replacement", + "Doc should report replacement subsequently (non-ASCII-compatible HTTP header should override detector)" + ); +} diff --git a/docshell/test/browser/browser_bug1716290-4.js b/docshell/test/browser/browser_bug1716290-4.js new file mode 100644 index 0000000000..ab0d46d7ff --- /dev/null +++ b/docshell/test/browser/browser_bug1716290-4.js @@ -0,0 +1,24 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug1716290-4.sjs", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.characterSet, + "Shift_JIS", + "Doc should report Shift_JIS initially" + ); +} + +function afterChangeCharset() { + is( + content.document.characterSet, + "UTF-16BE", + "Doc should report UTF-16BE subsequently (BOM should override detector)" + ); +} diff --git a/docshell/test/browser/browser_bug1719178.js b/docshell/test/browser/browser_bug1719178.js new file mode 100644 index 0000000000..6c4fc2d15f --- /dev/null +++ b/docshell/test/browser/browser_bug1719178.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; +SimpleTest.requestFlakyTimeout( + "The test needs to let objects die asynchronously." +); + +add_task(async function test_accessing_shistory() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences" + ); + let sh = tab.linkedBrowser.browsingContext.sessionHistory; + ok(sh, "Should have SessionHistory object"); + gBrowser.removeTab(tab); + tab = null; + for (let i = 0; i < 5; ++i) { + SpecialPowers.Services.obs.notifyObservers( + null, + "memory-pressure", + "heap-minimize" + ); + SpecialPowers.DOMWindowUtils.garbageCollect(); + await new Promise(function (r) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(r, 50); + }); + } + + try { + sh.reloadCurrentEntry(); + } catch (ex) {} + ok(true, "This test shouldn't crash."); +}); diff --git a/docshell/test/browser/browser_bug1736248-1.js b/docshell/test/browser/browser_bug1736248-1.js new file mode 100644 index 0000000000..d9cbe4fd85 --- /dev/null +++ b/docshell/test/browser/browser_bug1736248-1.js @@ -0,0 +1,34 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug1736248-1.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u00C3"), + 1064, + "Doc should be windows-1252 initially" + ); + is( + content.document.characterSet, + "windows-1252", + "Doc should report windows-1252 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u00E4"), + 1064, + "Doc should be UTF-8 subsequently" + ); + is( + content.document.characterSet, + "UTF-8", + "Doc should report UTF-8 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug1757005.js b/docshell/test/browser/browser_bug1757005.js new file mode 100644 index 0000000000..90010a9b7a --- /dev/null +++ b/docshell/test/browser/browser_bug1757005.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + // (1) Load one page with bfcache disabled and another one with bfcache enabled. + // (2) Check that BrowsingContext.getCurrentTopByBrowserId(browserId) returns + // the expected browsing context both in the parent process and in the child process. + // (3) Go back and then forward + // (4) Run the same checks as in step 2 again. + + let url1 = "data:text/html,<body onunload='/* disable bfcache */'>"; + let url2 = "data:text/html,page2"; + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: url1, + }, + async function (browser) { + info("Initial load"); + + let loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, url2); + await loaded; + info("Second page loaded"); + + let browserId = browser.browserId; + ok(!!browser.browsingContext, "Should have a BrowsingContext. (1)"); + is( + BrowsingContext.getCurrentTopByBrowserId(browserId), + browser.browsingContext, + "Should get the correct browsingContext(1)" + ); + + await ContentTask.spawn(browser, browserId, async function (browserId) { + Assert.ok( + BrowsingContext.getCurrentTopByBrowserId(browserId) == + docShell.browsingContext + ); + Assert.ok(docShell.browsingContext.browserId == browserId); + }); + + let awaitPageShow = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goBack(); + await awaitPageShow; + info("Back"); + + awaitPageShow = BrowserTestUtils.waitForContentEvent(browser, "pageshow"); + browser.goForward(); + await awaitPageShow; + info("Forward"); + + ok(!!browser.browsingContext, "Should have a BrowsingContext. (2)"); + is( + BrowsingContext.getCurrentTopByBrowserId(browserId), + browser.browsingContext, + "Should get the correct BrowsingContext. (2)" + ); + + await ContentTask.spawn(browser, browserId, async function (browserId) { + Assert.ok( + BrowsingContext.getCurrentTopByBrowserId(browserId) == + docShell.browsingContext + ); + Assert.ok(docShell.browsingContext.browserId == browserId); + }); + } + ); +}); diff --git a/docshell/test/browser/browser_bug1769189.js b/docshell/test/browser/browser_bug1769189.js new file mode 100644 index 0000000000..08cb4f9002 --- /dev/null +++ b/docshell/test/browser/browser_bug1769189.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_beforeUnload_and_replaceState() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "data:text/html,<script>window.addEventListener('beforeunload', () => { window.history.replaceState(true, ''); });</script>", + }, + async function (browser) { + let initialState = await SpecialPowers.spawn(browser, [], () => { + return content.history.state; + }); + + is(initialState, null, "history.state should be initially null."); + + let awaitPageShow = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + BrowserReload(); + await awaitPageShow; + + let updatedState = await SpecialPowers.spawn(browser, [], () => { + return content.history.state; + }); + is(updatedState, true, "history.state should have been updated."); + } + ); +}); diff --git a/docshell/test/browser/browser_bug1798780.js b/docshell/test/browser/browser_bug1798780.js new file mode 100644 index 0000000000..efc0ed9561 --- /dev/null +++ b/docshell/test/browser/browser_bug1798780.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// The test loads an initial page and then another page which does enough +// fragment navigations so that when going back to the initial page and then +// forward to the last page, the initial page is evicted from the bfcache. +add_task(async function testBFCacheEviction() { + // Make an unrealistic large timeout. + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionhistory.contentViewerTimeout", 86400]], + }); + + const uri = "data:text/html,initial page"; + const uri2 = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "dummy_page.html"; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: uri }, + async function (browser) { + BrowserTestUtils.loadURIString(browser, uri2); + await BrowserTestUtils.browserLoaded(browser); + + let awaitPageShow = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + await SpecialPowers.spawn(browser, [], async function () { + content.location.hash = "1"; + content.location.hash = "2"; + content.location.hash = "3"; + content.history.go(-4); + }); + + await awaitPageShow; + + let awaitPageShow2 = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + await SpecialPowers.spawn(browser, [], async function () { + content.history.go(4); + }); + await awaitPageShow2; + ok(true, "Didn't time out."); + } + ); +}); diff --git a/docshell/test/browser/browser_bug234628-1.js b/docshell/test/browser/browser_bug234628-1.js new file mode 100644 index 0000000000..566da65bca --- /dev/null +++ b/docshell/test/browser/browser_bug234628-1.js @@ -0,0 +1,47 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug234628-1.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 129, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 85, + "Child doc should be windows-1252 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 129, + "Parent doc should be windows-1252 subsequently" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 85, + "Child doc should be windows-1252 subsequently" + ); + + is( + content.document.characterSet, + "windows-1252", + "Parent doc should report windows-1252 subsequently" + ); + is( + content.frames[0].document.characterSet, + "windows-1252", + "Child doc should report windows-1252 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug234628-10.js b/docshell/test/browser/browser_bug234628-10.js new file mode 100644 index 0000000000..8fb51cf27c --- /dev/null +++ b/docshell/test/browser/browser_bug234628-10.js @@ -0,0 +1,46 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug234628-10.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 151, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 71, + "Child doc should be utf-8 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 151, + "Parent doc should be windows-1252 initially" + ); + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 71, + "Child doc should decode as utf-8 subsequently" + ); + + is( + content.document.characterSet, + "windows-1252", + "Parent doc should report windows-1252 subsequently" + ); + is( + content.frames[0].document.characterSet, + "UTF-8", + "Child doc should report UTF-8 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug234628-11.js b/docshell/test/browser/browser_bug234628-11.js new file mode 100644 index 0000000000..d11645ff76 --- /dev/null +++ b/docshell/test/browser/browser_bug234628-11.js @@ -0,0 +1,46 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug234628-11.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 193, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 107, + "Child doc should be utf-8 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 193, + "Parent doc should be windows-1252 subsequently" + ); + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 107, + "Child doc should decode as utf-8 subsequently" + ); + + is( + content.document.characterSet, + "windows-1252", + "Parent doc should report windows-1252 subsequently" + ); + is( + content.frames[0].document.characterSet, + "UTF-8", + "Child doc should report UTF-8 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug234628-2.js b/docshell/test/browser/browser_bug234628-2.js new file mode 100644 index 0000000000..da93dc2ac2 --- /dev/null +++ b/docshell/test/browser/browser_bug234628-2.js @@ -0,0 +1,49 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug234628-2.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 129, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf( + "\u00E2\u201A\u00AC" + ), + 78, + "Child doc should be windows-1252 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 129, + "Parent doc should be windows-1252 subsequently" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 78, + "Child doc should be UTF-8 subsequently" + ); + + is( + content.document.characterSet, + "windows-1252", + "Parent doc should report windows-1252 subsequently" + ); + is( + content.frames[0].document.characterSet, + "UTF-8", + "Child doc should report UTF-8 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug234628-3.js b/docshell/test/browser/browser_bug234628-3.js new file mode 100644 index 0000000000..8a143b51a6 --- /dev/null +++ b/docshell/test/browser/browser_bug234628-3.js @@ -0,0 +1,47 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug234628-3.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 118, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 73, + "Child doc should be utf-8 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 118, + "Parent doc should be windows-1252 subsequently" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 73, + "Child doc should be utf-8 subsequently" + ); + + is( + content.document.characterSet, + "windows-1252", + "Parent doc should report windows-1252 subsequently" + ); + is( + content.frames[0].document.characterSet, + "UTF-8", + "Child doc should report UTF-8 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug234628-4.js b/docshell/test/browser/browser_bug234628-4.js new file mode 100644 index 0000000000..19ec0f8dbf --- /dev/null +++ b/docshell/test/browser/browser_bug234628-4.js @@ -0,0 +1,46 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug234628-4.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 132, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 79, + "Child doc should be utf-8 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 132, + "Parent doc should decode as windows-1252 subsequently" + ); + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 79, + "Child doc should decode as utf-8 subsequently" + ); + + is( + content.document.characterSet, + "windows-1252", + "Parent doc should report windows-1252 subsequently" + ); + is( + content.frames[0].document.characterSet, + "UTF-8", + "Child doc should report UTF-8 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug234628-5.js b/docshell/test/browser/browser_bug234628-5.js new file mode 100644 index 0000000000..77753ed78d --- /dev/null +++ b/docshell/test/browser/browser_bug234628-5.js @@ -0,0 +1,46 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug234628-5.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 146, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 87, + "Child doc should be utf-16 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 146, + "Parent doc should be windows-1252 subsequently" + ); + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 87, + "Child doc should decode as utf-16 subsequently" + ); + + is( + content.document.characterSet, + "windows-1252", + "Parent doc should report windows-1252 subsequently" + ); + is( + content.frames[0].document.characterSet, + "UTF-16LE", + "Child doc should report UTF-16LE subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug234628-6.js b/docshell/test/browser/browser_bug234628-6.js new file mode 100644 index 0000000000..88ff6c1a82 --- /dev/null +++ b/docshell/test/browser/browser_bug234628-6.js @@ -0,0 +1,47 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug234628-6.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 190, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 109, + "Child doc should be utf-16 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 190, + "Parent doc should be windows-1252 subsequently" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 109, + "Child doc should be utf-16 subsequently" + ); + + is( + content.document.characterSet, + "windows-1252", + "Parent doc should report windows-1252 subsequently" + ); + is( + content.frames[0].document.characterSet, + "UTF-16BE", + "Child doc should report UTF-16 subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug234628-8.js b/docshell/test/browser/browser_bug234628-8.js new file mode 100644 index 0000000000..024a3d4d64 --- /dev/null +++ b/docshell/test/browser/browser_bug234628-8.js @@ -0,0 +1,18 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetCheck(rootDir + "file_bug234628-8.html", afterOpen); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u0402"), + 156, + "Parent doc should be windows-1251" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u0402"), + 99, + "Child doc should be windows-1251" + ); +} diff --git a/docshell/test/browser/browser_bug234628-9.js b/docshell/test/browser/browser_bug234628-9.js new file mode 100644 index 0000000000..ceb7dc4e63 --- /dev/null +++ b/docshell/test/browser/browser_bug234628-9.js @@ -0,0 +1,18 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetCheck(rootDir + "file_bug234628-9.html", afterOpen); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u20AC"), + 145, + "Parent doc should be UTF-16" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u20AC"), + 96, + "Child doc should be windows-1252" + ); +} diff --git a/docshell/test/browser/browser_bug349769.js b/docshell/test/browser/browser_bug349769.js new file mode 100644 index 0000000000..92e8f78543 --- /dev/null +++ b/docshell/test/browser/browser_bug349769.js @@ -0,0 +1,79 @@ +add_task(async function test() { + const uris = [undefined, "about:blank"]; + + function checkContentProcess(newBrowser, uri) { + return ContentTask.spawn(newBrowser, [uri], async function (uri) { + var prin = content.document.nodePrincipal; + Assert.notEqual( + prin, + null, + "Loaded principal must not be null when adding " + uri + ); + Assert.notEqual( + prin, + undefined, + "Loaded principal must not be undefined when loading " + uri + ); + + Assert.equal( + prin.isSystemPrincipal, + false, + "Loaded principal must not be system when loading " + uri + ); + }); + } + + for (var uri of uris) { + await BrowserTestUtils.withNewTab( + { gBrowser }, + async function (newBrowser) { + let loadedPromise = BrowserTestUtils.browserLoaded(newBrowser); + BrowserTestUtils.loadURIString(newBrowser, uri); + + var prin = newBrowser.contentPrincipal; + isnot( + prin, + null, + "Forced principal must not be null when loading " + uri + ); + isnot( + prin, + undefined, + "Forced principal must not be undefined when loading " + uri + ); + is( + prin.isSystemPrincipal, + false, + "Forced principal must not be system when loading " + uri + ); + + // Belt-and-suspenders e10s check: make sure that the same checks hold + // true in the content process. + await checkContentProcess(newBrowser, uri); + + await loadedPromise; + + prin = newBrowser.contentPrincipal; + isnot( + prin, + null, + "Loaded principal must not be null when adding " + uri + ); + isnot( + prin, + undefined, + "Loaded principal must not be undefined when loading " + uri + ); + is( + prin.isSystemPrincipal, + false, + "Loaded principal must not be system when loading " + uri + ); + + // Belt-and-suspenders e10s check: make sure that the same checks hold + // true in the content process. + await checkContentProcess(newBrowser, uri); + } + ); + } +}); diff --git a/docshell/test/browser/browser_bug388121-1.js b/docshell/test/browser/browser_bug388121-1.js new file mode 100644 index 0000000000..07c476f11b --- /dev/null +++ b/docshell/test/browser/browser_bug388121-1.js @@ -0,0 +1,22 @@ +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function (newBrowser) { + await SpecialPowers.spawn(newBrowser, [], async function () { + var prin = content.document.nodePrincipal; + Assert.notEqual(prin, null, "Loaded principal must not be null"); + Assert.notEqual( + prin, + undefined, + "Loaded principal must not be undefined" + ); + + Assert.equal( + prin.isSystemPrincipal, + false, + "Loaded principal must not be system" + ); + }); + } + ); +}); diff --git a/docshell/test/browser/browser_bug388121-2.js b/docshell/test/browser/browser_bug388121-2.js new file mode 100644 index 0000000000..695ff1079c --- /dev/null +++ b/docshell/test/browser/browser_bug388121-2.js @@ -0,0 +1,74 @@ +function test() { + waitForExplicitFinish(); + + var w; + var iteration = 1; + const uris = ["", "about:blank"]; + var uri; + var origWgp; + + function testLoad() { + let wgp = w.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; + if (wgp == origWgp) { + // Go back to polling + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(testLoad, 10); + return; + } + var prin = wgp.documentPrincipal; + isnot(prin, null, "Loaded principal must not be null when adding " + uri); + isnot( + prin, + undefined, + "Loaded principal must not be undefined when loading " + uri + ); + is( + prin.isSystemPrincipal, + false, + "Loaded principal must not be system when loading " + uri + ); + w.close(); + + if (iteration == uris.length) { + finish(); + } else { + ++iteration; + doTest(); + } + } + + function doTest() { + uri = uris[iteration - 1]; + window.open(uri, "_blank", "width=10,height=10,noopener"); + w = Services.wm.getMostRecentWindow("navigator:browser"); + origWgp = w.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; + var prin = origWgp.documentPrincipal; + if (!uri) { + uri = undefined; + } + isnot(prin, null, "Forced principal must not be null when loading " + uri); + isnot( + prin, + undefined, + "Forced principal must not be undefined when loading " + uri + ); + is( + prin.isSystemPrincipal, + false, + "Forced principal must not be system when loading " + uri + ); + if (uri == undefined) { + // No actual load here, so just move along. + w.close(); + ++iteration; + doTest(); + } else { + // Need to poll, because load listeners on the content window won't + // survive the load. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(testLoad, 10); + } + } + + doTest(); +} diff --git a/docshell/test/browser/browser_bug420605.js b/docshell/test/browser/browser_bug420605.js new file mode 100644 index 0000000000..9a18ab2c9f --- /dev/null +++ b/docshell/test/browser/browser_bug420605.js @@ -0,0 +1,131 @@ +/* Test for Bug 420605 + * https://bugzilla.mozilla.org/show_bug.cgi?id=420605 + */ + +const { PlacesTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PlacesTestUtils.sys.mjs" +); + +add_task(async function test() { + var pageurl = + "http://mochi.test:8888/browser/docshell/test/browser/file_bug420605.html"; + var fragmenturl = + "http://mochi.test:8888/browser/docshell/test/browser/file_bug420605.html#firefox"; + + /* Queries nsINavHistoryService and returns a single history entry + * for a given URI */ + function getNavHistoryEntry(aURI) { + var options = PlacesUtils.history.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + options.maxResults = 1; + + var query = PlacesUtils.history.getNewQuery(); + query.uri = aURI; + var result = PlacesUtils.history.executeQuery(query, options); + result.root.containerOpen = true; + + if (!result.root.childCount) { + return null; + } + return result.root.getChild(0); + } + + // We'll save the favicon URL of the orignal page here and check that the + // page with a hash has the same favicon. + var originalFavicon; + + // Control flow in this test is a bit complicated. + // + // When the page loads, onPageLoad (the DOMContentLoaded handler) and + // favicon-changed are both called, in some order. Once + // they've both run, we click a fragment link in the content page + // (clickLinkIfReady), which should trigger another favicon-changed event, + // this time for the fragment's URL. + + var _clickLinkTimes = 0; + function clickLinkIfReady() { + _clickLinkTimes++; + if (_clickLinkTimes == 2) { + BrowserTestUtils.synthesizeMouseAtCenter( + "#firefox-link", + {}, + gBrowser.selectedBrowser + ); + } + } + + function onPageLoad() { + clickLinkIfReady(); + } + + // Make sure neither of the test pages haven't been loaded before. + var info = getNavHistoryEntry(makeURI(pageurl)); + ok(!info, "The test page must not have been visited already."); + info = getNavHistoryEntry(makeURI(fragmenturl)); + ok(!info, "The fragment test page must not have been visited already."); + + let promiseIcon1 = PlacesTestUtils.waitForNotification( + "favicon-changed", + events => + events.some(e => { + if (e.url == pageurl) { + ok( + e.faviconUrl, + "Favicon value is not null for page without fragment." + ); + originalFavicon = e.faviconUrl; + + // Now that the favicon has loaded, click on fragment link. + // This should trigger the |case fragmenturl| below. + clickLinkIfReady(); + return true; + } + return false; + }) + ); + let promiseIcon2 = PlacesTestUtils.waitForNotification( + "favicon-changed", + events => + events.some(e => { + if (e.url == fragmenturl) { + // If the fragment URL's favicon isn't set, this branch won't + // be called and the test will time out. + is( + e.faviconUrl, + originalFavicon, + "New favicon should be same as original favicon." + ); + ok( + e.faviconUrl, + "Favicon value is not null for page without fragment." + ); + originalFavicon = e.faviconUrl; + + // Now that the favicon has loaded, click on fragment link. + // This should trigger the |case fragmenturl| below. + clickLinkIfReady(); + return true; + } + return false; + }) + ); + + // Now open the test page in a new tab. + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "DOMContentLoaded", + true + ).then(onPageLoad); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, pageurl); + + await promiseIcon1; + await promiseIcon2; + + // Let's explicitly check that we can get the favicon + // from nsINavHistoryService now. + info = getNavHistoryEntry(makeURI(fragmenturl)); + ok(info, "There must be a history entry for the fragment."); + ok(info.icon, "The history entry must have an associated favicon."); + gBrowser.removeCurrentTab(); +}); diff --git a/docshell/test/browser/browser_bug422543.js b/docshell/test/browser/browser_bug422543.js new file mode 100644 index 0000000000..d3b772619e --- /dev/null +++ b/docshell/test/browser/browser_bug422543.js @@ -0,0 +1,253 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const ACTOR = "Bug422543"; + +let getActor = browser => { + return browser.browsingContext.currentWindowGlobal.getActor(ACTOR); +}; + +add_task(async function runTests() { + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await setupAsync(); + let browser = gBrowser.selectedBrowser; + // Now that we're set up, initialize our frame script. + await checkListenersAsync("initial", "listeners initialized"); + + // Check if all history listeners are always notified. + info("# part 1"); + await whenPageShown(browser, () => + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + BrowserTestUtils.loadURIString(browser, "http://www.example.com/") + ); + await checkListenersAsync("newentry", "shistory has a new entry"); + ok(browser.canGoBack, "we can go back"); + + await whenPageShown(browser, () => browser.goBack()); + await checkListenersAsync("gotoindex", "back to the first shentry"); + ok(browser.canGoForward, "we can go forward"); + + await whenPageShown(browser, () => browser.goForward()); + await checkListenersAsync("gotoindex", "forward to the second shentry"); + + await whenPageShown(browser, () => browser.reload()); + await checkListenersAsync("reload", "current shentry reloaded"); + + await whenPageShown(browser, () => browser.gotoIndex(0)); + await checkListenersAsync("gotoindex", "back to the first index"); + + // Check nsISHistory.notifyOnHistoryReload + info("# part 2"); + ok(await notifyReloadAsync(), "reloading has not been canceled"); + await checkListenersAsync("reload", "saw the reload notification"); + + // Let the first listener cancel the reload action. + info("# part 3"); + await resetListenersAsync(); + await setListenerRetvalAsync(0, false); + ok(!(await notifyReloadAsync()), "reloading has been canceled"); + await checkListenersAsync("reload", "saw the reload notification"); + + // Let both listeners cancel the reload action. + info("# part 4"); + await resetListenersAsync(); + await setListenerRetvalAsync(1, false); + ok(!(await notifyReloadAsync()), "reloading has been canceled"); + await checkListenersAsync("reload", "saw the reload notification"); + + // Let the second listener cancel the reload action. + info("# part 5"); + await resetListenersAsync(); + await setListenerRetvalAsync(0, true); + ok(!(await notifyReloadAsync()), "reloading has been canceled"); + await checkListenersAsync("reload", "saw the reload notification"); + + function sendQuery(message, arg = {}) { + return getActor(gBrowser.selectedBrowser).sendQuery(message, arg); + } + + function checkListenersAsync(aLast, aMessage) { + return sendQuery("getListenerStatus").then(listenerStatuses => { + is(listenerStatuses[0], aLast, aMessage); + is(listenerStatuses[1], aLast, aMessage); + }); + } + + function resetListenersAsync() { + return sendQuery("resetListeners"); + } + + function notifyReloadAsync() { + return sendQuery("notifyReload").then(({ rval }) => { + return rval; + }); + } + + function setListenerRetvalAsync(num, val) { + return sendQuery("setRetval", { num, val }); + } + + async function setupAsync() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://mochi.test:8888" + ); + + let base = getRootDirectory(gTestPath).slice(0, -1); + ChromeUtils.registerWindowActor(ACTOR, { + child: { + esModuleURI: `${base}/Bug422543Child.sys.mjs`, + }, + }); + + registerCleanupFunction(async () => { + await sendQuery("cleanup"); + gBrowser.removeTab(tab); + + ChromeUtils.unregisterWindowActor(ACTOR); + }); + + await sendQuery("init"); + } + return; + } + + await setup(); + let browser = gBrowser.selectedBrowser; + // Now that we're set up, initialize our frame script. + checkListeners("initial", "listeners initialized"); + + // Check if all history listeners are always notified. + info("# part 1"); + await whenPageShown(browser, () => + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + BrowserTestUtils.loadURIString(browser, "http://www.example.com/") + ); + checkListeners("newentry", "shistory has a new entry"); + ok(browser.canGoBack, "we can go back"); + + await whenPageShown(browser, () => browser.goBack()); + checkListeners("gotoindex", "back to the first shentry"); + ok(browser.canGoForward, "we can go forward"); + + await whenPageShown(browser, () => browser.goForward()); + checkListeners("gotoindex", "forward to the second shentry"); + + await whenPageShown(browser, () => browser.reload()); + checkListeners("reload", "current shentry reloaded"); + + await whenPageShown(browser, () => browser.gotoIndex(0)); + checkListeners("gotoindex", "back to the first index"); + + // Check nsISHistory.notifyOnHistoryReload + info("# part 2"); + ok(notifyReload(browser), "reloading has not been canceled"); + checkListeners("reload", "saw the reload notification"); + + // Let the first listener cancel the reload action. + info("# part 3"); + resetListeners(); + setListenerRetval(0, false); + ok(!notifyReload(browser), "reloading has been canceled"); + checkListeners("reload", "saw the reload notification"); + + // Let both listeners cancel the reload action. + info("# part 4"); + resetListeners(); + setListenerRetval(1, false); + ok(!notifyReload(browser), "reloading has been canceled"); + checkListeners("reload", "saw the reload notification"); + + // Let the second listener cancel the reload action. + info("# part 5"); + resetListeners(); + setListenerRetval(0, true); + ok(!notifyReload(browser), "reloading has been canceled"); + checkListeners("reload", "saw the reload notification"); +}); + +class SHistoryListener { + constructor() { + this.retval = true; + this.last = "initial"; + } + + OnHistoryNewEntry(aNewURI) { + this.last = "newentry"; + } + + OnHistoryGotoIndex() { + this.last = "gotoindex"; + } + + OnHistoryPurge() { + this.last = "purge"; + } + + OnHistoryReload() { + this.last = "reload"; + return this.retval; + } + + OnHistoryReplaceEntry() {} +} +SHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", +]); + +let listeners = [new SHistoryListener(), new SHistoryListener()]; + +function checkListeners(aLast, aMessage) { + is(listeners[0].last, aLast, aMessage); + is(listeners[1].last, aLast, aMessage); +} + +function resetListeners() { + for (let listener of listeners) { + listener.last = "initial"; + } +} + +function notifyReload(browser) { + return browser.browsingContext.sessionHistory.notifyOnHistoryReload(); +} + +function setListenerRetval(num, val) { + listeners[num].retval = val; +} + +async function setup() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://mochi.test:8888" + ); + + let browser = tab.linkedBrowser; + registerCleanupFunction(async function () { + for (let listener of listeners) { + browser.browsingContext.sessionHistory.removeSHistoryListener(listener); + } + gBrowser.removeTab(tab); + }); + for (let listener of listeners) { + browser.browsingContext.sessionHistory.addSHistoryListener(listener); + } +} + +function whenPageShown(aBrowser, aNavigation) { + let promise = new Promise(resolve => { + let unregister = BrowserTestUtils.addContentEventListener( + aBrowser, + "pageshow", + () => { + unregister(); + resolve(); + }, + { capture: true } + ); + }); + + aNavigation(); + return promise; +} diff --git a/docshell/test/browser/browser_bug441169.js b/docshell/test/browser/browser_bug441169.js new file mode 100644 index 0000000000..09e83b040c --- /dev/null +++ b/docshell/test/browser/browser_bug441169.js @@ -0,0 +1,44 @@ +/* Make sure that netError won't allow HTML injection through badcert parameters. See bug 441169. */ +var newBrowser; + +function task() { + let resolve; + let promise = new Promise(r => { + resolve = r; + }); + + addEventListener("DOMContentLoaded", checkPage, false); + + function checkPage(event) { + if (event.target != content.document) { + return; + } + removeEventListener("DOMContentLoaded", checkPage, false); + + is( + content.document.getElementById("test_span"), + null, + "Error message should not be parsed as HTML, and hence shouldn't include the 'test_span' element." + ); + resolve(); + } + + var chromeURL = + "about:neterror?e=nssBadCert&u=https%3A//test.kuix.de/&c=UTF-8&d=This%20sentence%20should%20not%20be%20parsed%20to%20include%20a%20%3Cspan%20id=%22test_span%22%3Enamed%3C/span%3E%20span%20tag.%0A%0AThe%20certificate%20is%20only%20valid%20for%20%3Ca%20id=%22cert_domain_link%22%20title=%22kuix.de%22%3Ekuix.de%3C/a%3E%0A%0A(Error%20code%3A%20ssl_error_bad_cert_domain)"; + content.location = chromeURL; + + return promise; +} + +function test() { + waitForExplicitFinish(); + + var newTab = BrowserTestUtils.addTab(gBrowser); + gBrowser.selectedTab = newTab; + newBrowser = gBrowser.getBrowserForTab(newTab); + + ContentTask.spawn(newBrowser, null, task).then(() => { + gBrowser.removeCurrentTab(); + finish(); + }); +} diff --git a/docshell/test/browser/browser_bug503832.js b/docshell/test/browser/browser_bug503832.js new file mode 100644 index 0000000000..8699eb01fc --- /dev/null +++ b/docshell/test/browser/browser_bug503832.js @@ -0,0 +1,76 @@ +/* Test for Bug 503832 + * https://bugzilla.mozilla.org/show_bug.cgi?id=503832 + */ + +add_task(async function () { + var pagetitle = "Page Title for Bug 503832"; + var pageurl = + "http://mochi.test:8888/browser/docshell/test/browser/file_bug503832.html"; + var fragmenturl = + "http://mochi.test:8888/browser/docshell/test/browser/file_bug503832.html#firefox"; + + var historyService = Cc[ + "@mozilla.org/browser/nav-history-service;1" + ].getService(Ci.nsINavHistoryService); + + let fragmentPromise = new Promise(resolve => { + const listener = events => { + const { url, title } = events[0]; + + switch (url) { + case pageurl: + is(title, pagetitle, "Correct page title for " + url); + return; + case fragmenturl: + is(title, pagetitle, "Correct page title for " + url); + // If titles for fragment URLs aren't set, this code + // branch won't be called and the test will timeout, + // resulting in a failure + PlacesObservers.removeListener(["page-title-changed"], listener); + resolve(); + } + }; + + PlacesObservers.addListener(["page-title-changed"], listener); + }); + + /* Queries nsINavHistoryService and returns a single history entry + * for a given URI */ + function getNavHistoryEntry(aURI) { + var options = historyService.getNewQueryOptions(); + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; + options.maxResults = 1; + + var query = historyService.getNewQuery(); + query.uri = aURI; + + var result = historyService.executeQuery(query, options); + result.root.containerOpen = true; + + if (!result.root.childCount) { + return null; + } + var node = result.root.getChild(0); + result.root.containerOpen = false; + return node; + } + + // Make sure neither of the test pages haven't been loaded before. + var info = getNavHistoryEntry(makeURI(pageurl)); + ok(!info, "The test page must not have been visited already."); + info = getNavHistoryEntry(makeURI(fragmenturl)); + ok(!info, "The fragment test page must not have been visited already."); + + // Now open the test page in a new tab + await BrowserTestUtils.openNewForegroundTab(gBrowser, pageurl); + + // Now that the page is loaded, click on fragment link + await BrowserTestUtils.synthesizeMouseAtCenter( + "#firefox-link", + {}, + gBrowser.selectedBrowser + ); + await fragmentPromise; + + gBrowser.removeCurrentTab(); +}); diff --git a/docshell/test/browser/browser_bug554155.js b/docshell/test/browser/browser_bug554155.js new file mode 100644 index 0000000000..223f6241de --- /dev/null +++ b/docshell/test/browser/browser_bug554155.js @@ -0,0 +1,33 @@ +add_task(async function test() { + await BrowserTestUtils.withNewTab( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + { gBrowser, url: "http://example.com" }, + async function (browser) { + let numLocationChanges = 0; + + let listener = { + onLocationChange(browser, webProgress, request, uri, flags) { + info("location change: " + (uri && uri.spec)); + numLocationChanges++; + }, + }; + + gBrowser.addTabsProgressListener(listener); + + await SpecialPowers.spawn(browser, [], function () { + // pushState to a new URL (http://example.com/foo"). This should trigger + // exactly one LocationChange event. + content.history.pushState(null, null, "foo"); + }); + + await Promise.resolve(); + + gBrowser.removeTabsProgressListener(listener); + is( + numLocationChanges, + 1, + "pushState should cause exactly one LocationChange event." + ); + } + ); +}); diff --git a/docshell/test/browser/browser_bug655270.js b/docshell/test/browser/browser_bug655270.js new file mode 100644 index 0000000000..3120abb269 --- /dev/null +++ b/docshell/test/browser/browser_bug655270.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test for Bug 655273 + * + * Call pushState and then make sure that the favicon service associates our + * old favicon with the new URI. + */ + +const { PlacesTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PlacesTestUtils.sys.mjs" +); + +add_task(async function test() { + const testDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + const origURL = testDir + "file_bug655270.html"; + const newURL = origURL + "?new_page"; + + const faviconURL = testDir + "favicon_bug655270.ico"; + + let icon1; + let promiseIcon1 = PlacesTestUtils.waitForNotification( + "favicon-changed", + events => + events.some(e => { + if (e.url == origURL) { + icon1 = e.faviconUrl; + return true; + } + return false; + }) + ); + let icon2; + let promiseIcon2 = PlacesTestUtils.waitForNotification( + "favicon-changed", + events => + events.some(e => { + if (e.url == newURL) { + icon2 = e.faviconUrl; + return true; + } + return false; + }) + ); + + // The page at origURL has a <link rel='icon'>, so we should get a call into + // our observer below when it loads. Once we verify that we have the right + // favicon URI, we call pushState, which should trigger another favicon change + // event, this time for the URI after pushState. + let tab = BrowserTestUtils.addTab(gBrowser, origURL); + await promiseIcon1; + is(icon1, faviconURL, "FaviconURL for original URI"); + // Ignore the promise returned here and wait for the next + // onPageChanged notification. + SpecialPowers.spawn(tab.linkedBrowser, [], function () { + content.history.pushState("", "", "?new_page"); + }); + await promiseIcon2; + is(icon2, faviconURL, "FaviconURL for new URI"); + gBrowser.removeTab(tab); +}); diff --git a/docshell/test/browser/browser_bug655273.js b/docshell/test/browser/browser_bug655273.js new file mode 100644 index 0000000000..e564eb2f6c --- /dev/null +++ b/docshell/test/browser/browser_bug655273.js @@ -0,0 +1,56 @@ +/* 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/. */ + +/** + * Test for Bug 655273. Make sure that after changing the URI via + * history.pushState, the resulting SHEntry has the same title as our old + * SHEntry. + */ + +add_task(async function test() { + waitForExplicitFinish(); + + await BrowserTestUtils.withNewTab( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + { gBrowser, url: "http://example.com" }, + async function (browser) { + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await SpecialPowers.spawn(browser, [], async function () { + let cw = content; + let oldTitle = cw.document.title; + ok(oldTitle, "Content window should initially have a title."); + cw.history.pushState("", "", "new_page"); + + let shistory = cw.docShell.QueryInterface( + Ci.nsIWebNavigation + ).sessionHistory; + + is( + shistory.legacySHistory.getEntryAtIndex(shistory.index).title, + oldTitle, + "SHEntry title after pushstate." + ); + }); + + return; + } + + let bc = browser.browsingContext; + let oldTitle = browser.browsingContext.currentWindowGlobal.documentTitle; + ok(oldTitle, "Content window should initially have a title."); + SpecialPowers.spawn(browser, [], async function () { + content.history.pushState("", "", "new_page"); + }); + + let shistory = bc.sessionHistory; + await SHListener.waitForHistory(shistory, SHListener.NewEntry); + + is( + shistory.getEntryAtIndex(shistory.index).title, + oldTitle, + "SHEntry title after pushstate." + ); + } + ); +}); diff --git a/docshell/test/browser/browser_bug670318.js b/docshell/test/browser/browser_bug670318.js new file mode 100644 index 0000000000..06c2a10f79 --- /dev/null +++ b/docshell/test/browser/browser_bug670318.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test for Bug 670318 + * + * When LoadEntry() is called on a browser that has multiple duplicate history + * entries, history.index can end up out of range (>= history.count). + */ + +const URL = + "http://mochi.test:8888/browser/docshell/test/browser/file_bug670318.html"; + +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function (browser) { + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + await ContentTask.spawn(browser, URL, async function (URL) { + let history = docShell.QueryInterface( + Ci.nsIWebNavigation + ).sessionHistory; + let count = 0; + + let testDone = {}; + testDone.promise = new Promise(resolve => { + testDone.resolve = resolve; + }); + + // Since listener implements nsISupportsWeakReference, we are + // responsible for keeping it alive so that the GC doesn't clear + // it before the test completes. We do this by anchoring the listener + // to the message manager, and clearing it just before the test + // completes. + this._testListener = { + owner: this, + OnHistoryNewEntry(aNewURI) { + info("OnHistoryNewEntry " + aNewURI.spec + ", " + count); + if (aNewURI.spec == URL && 5 == ++count) { + addEventListener( + "load", + function onLoad() { + Assert.ok( + history.index < history.count, + "history.index is valid" + ); + testDone.resolve(); + }, + { capture: true, once: true } + ); + + history.legacySHistory.removeSHistoryListener( + this.owner._testListener + ); + delete this.owner._testListener; + this.owner = null; + content.setTimeout(() => { + content.location.reload(); + }, 0); + } + }, + + OnHistoryReload: () => true, + OnHistoryGotoIndex: () => {}, + OnHistoryPurge: () => {}, + OnHistoryReplaceEntry: () => { + // The initial load of about:blank causes a transient entry to be + // created, so our first navigation to a real page is a replace + // instead of a new entry. + ++count; + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + + history.legacySHistory.addSHistoryListener(this._testListener); + content.location = URL; + + await testDone.promise; + }); + + return; + } + + let history = browser.browsingContext.sessionHistory; + let count = 0; + + let testDone = {}; + testDone.promise = new Promise(resolve => { + testDone.resolve = resolve; + }); + + let listener = { + async OnHistoryNewEntry(aNewURI) { + if (aNewURI.spec == URL && 5 == ++count) { + history.removeSHistoryListener(listener); + await ContentTask.spawn(browser, null, () => { + return new Promise(resolve => { + addEventListener( + "load", + evt => { + let history = docShell.QueryInterface( + Ci.nsIWebNavigation + ).sessionHistory; + Assert.ok( + history.index < history.count, + "history.index is valid" + ); + resolve(); + }, + { capture: true, once: true } + ); + + content.location.reload(); + }); + }); + testDone.resolve(); + } + }, + + OnHistoryReload: () => true, + OnHistoryGotoIndex: () => {}, + OnHistoryPurge: () => {}, + OnHistoryReplaceEntry: () => { + // The initial load of about:blank causes a transient entry to be + // created, so our first navigation to a real page is a replace + // instead of a new entry. + ++count; + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + + history.addSHistoryListener(listener); + BrowserTestUtils.loadURIString(browser, URL); + + await testDone.promise; + } + ); +}); diff --git a/docshell/test/browser/browser_bug673087-1.js b/docshell/test/browser/browser_bug673087-1.js new file mode 100644 index 0000000000..427b246d76 --- /dev/null +++ b/docshell/test/browser/browser_bug673087-1.js @@ -0,0 +1,46 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug673087-1.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\u00A4"), + 151, + "Parent doc should be windows-1252 initially" + ); + + is( + content.frames[0].document.documentElement.textContent.indexOf("\u00A4"), + 95, + "Child doc should be windows-1252 initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\u3042"), + 151, + "Parent doc should decode as EUC-JP subsequently" + ); + is( + content.frames[0].document.documentElement.textContent.indexOf("\u3042"), + 95, + "Child doc should decode as EUC-JP subsequently" + ); + + is( + content.document.characterSet, + "EUC-JP", + "Parent doc should report EUC-JP subsequently" + ); + is( + content.frames[0].document.characterSet, + "EUC-JP", + "Child doc should report EUC-JP subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug673087-2.js b/docshell/test/browser/browser_bug673087-2.js new file mode 100644 index 0000000000..13a7a2a82c --- /dev/null +++ b/docshell/test/browser/browser_bug673087-2.js @@ -0,0 +1,36 @@ +function test() { + var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + runCharsetTest( + rootDir + "file_bug673087-2.html", + afterOpen, + afterChangeCharset + ); +} + +function afterOpen() { + is( + content.document.documentElement.textContent.indexOf("\uFFFD"), + 0, + "Doc should decode as replacement initially" + ); + + is( + content.document.characterSet, + "replacement", + "Doc should report replacement initially" + ); +} + +function afterChangeCharset() { + is( + content.document.documentElement.textContent.indexOf("\uFFFD"), + 0, + "Doc should decode as replacement subsequently" + ); + + is( + content.document.characterSet, + "replacement", + "Doc should report replacement subsequently" + ); +} diff --git a/docshell/test/browser/browser_bug673467.js b/docshell/test/browser/browser_bug673467.js new file mode 100644 index 0000000000..507410254c --- /dev/null +++ b/docshell/test/browser/browser_bug673467.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test for bug 673467. In a new tab, load a page which inserts a new iframe +// before the load and then sets its location during the load. This should +// create just one SHEntry. + +var doc = + "data:text/html,<html><body onload='load()'>" + + "<script>" + + " var iframe = document.createElement('iframe');" + + " iframe.id = 'iframe';" + + " document.documentElement.appendChild(iframe);" + + " function load() {" + + " iframe.src = 'data:text/html,Hello!';" + + " }" + + "</script>" + + "</body></html>"; + +function test() { + waitForExplicitFinish(); + + let taskFinished; + + let tab = BrowserTestUtils.addTab(gBrowser, doc, {}, tab => { + taskFinished = ContentTask.spawn(tab.linkedBrowser, null, () => { + return new Promise(resolve => { + addEventListener( + "load", + function () { + // The main page has loaded. Now wait for the iframe to load. + let iframe = content.document.getElementById("iframe"); + iframe.addEventListener( + "load", + function listener(aEvent) { + // Wait for the iframe to load the new document, not about:blank. + if (!iframe.src) { + return; + } + + iframe.removeEventListener("load", listener, true); + let shistory = content.docShell.QueryInterface( + Ci.nsIWebNavigation + ).sessionHistory; + + Assert.equal(shistory.count, 1, "shistory count should be 1."); + resolve(); + }, + true + ); + }, + true + ); + }); + }); + }); + + taskFinished.then(() => { + gBrowser.removeTab(tab); + finish(); + }); +} diff --git a/docshell/test/browser/browser_bug852909.js b/docshell/test/browser/browser_bug852909.js new file mode 100644 index 0000000000..108baa0626 --- /dev/null +++ b/docshell/test/browser/browser_bug852909.js @@ -0,0 +1,35 @@ +var rootDir = "http://mochi.test:8888/browser/docshell/test/browser/"; + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + rootDir + "file_bug852909.png" + ); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(image); +} + +function image(event) { + ok( + !gBrowser.selectedTab.mayEnableCharacterEncodingMenu, + "Docshell should say the menu should be disabled for images." + ); + + gBrowser.removeCurrentTab(); + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + rootDir + "file_bug852909.pdf" + ); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(pdf); +} + +function pdf(event) { + ok( + !gBrowser.selectedTab.mayEnableCharacterEncodingMenu, + "Docshell should say the menu should be disabled for PDF.js." + ); + + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/docshell/test/browser/browser_bug92473.js b/docshell/test/browser/browser_bug92473.js new file mode 100644 index 0000000000..7e386f5ee9 --- /dev/null +++ b/docshell/test/browser/browser_bug92473.js @@ -0,0 +1,70 @@ +/* The test text as octets for reference + * %83%86%83%6a%83%52%81%5b%83%68%82%cd%81%41%82%b7%82%d7%82%c4%82%cc%95%b6%8e%9a%82%c9%8c%c5%97%4c%82%cc%94%d4%8d%86%82%f0%95%74%97%5e%82%b5%82%dc%82%b7 + */ + +function testContent(text) { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [text], text => { + Assert.equal( + content.document.getElementById("testpar").innerHTML, + text, + "<p> contains expected text" + ); + Assert.equal( + content.document.getElementById("testtextarea").innerHTML, + text, + "<textarea> contains expected text" + ); + Assert.equal( + content.document.getElementById("testinput").value, + text, + "<input> contains expected text" + ); + }); +} + +function afterOpen() { + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then( + afterChangeCharset + ); + + /* The test text decoded incorrectly as Windows-1251. This is the "right" wrong + text; anything else is unexpected. */ + const wrongText = + "\u0453\u2020\u0453\u006A\u0453\u0052\u0403\u005B\u0453\u0068\u201A\u041D\u0403\u0041\u201A\u00B7\u201A\u0427\u201A\u0414\u201A\u041C\u2022\u00B6\u040B\u0459\u201A\u0419\u040A\u0415\u2014\u004C\u201A\u041C\u201D\u0424\u040C\u2020\u201A\u0440\u2022\u0074\u2014\u005E\u201A\u00B5\u201A\u042C\u201A\u00B7"; + + /* Test that the content on load is the expected wrong decoding */ + testContent(wrongText).then(() => { + BrowserForceEncodingDetection(); + }); +} + +function afterChangeCharset() { + /* The test text decoded correctly as Shift_JIS */ + const rightText = + "\u30E6\u30CB\u30B3\u30FC\u30C9\u306F\u3001\u3059\u3079\u3066\u306E\u6587\u5B57\u306B\u56FA\u6709\u306E\u756A\u53F7\u3092\u4ED8\u4E0E\u3057\u307E\u3059"; + + /* test that the content is decoded correctly */ + testContent(rightText).then(() => { + gBrowser.removeCurrentTab(); + finish(); + }); +} + +function test() { + waitForExplicitFinish(); + + // Get the local directory. This needs to be a file: URI because chrome: URIs + // are always UTF-8 (bug 617339) and we are testing decoding from other + // charsets. + var jar = getJar(getRootDirectory(gTestPath)); + var dir = jar + ? extractJarToTmp(jar) + : getChromeDir(getResolvedURI(gTestPath)); + var rootDir = Services.io.newFileURI(dir).spec; + + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + rootDir + "test-form_sjis.html" + ); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(afterOpen); +} diff --git a/docshell/test/browser/browser_click_link_within_view_source.js b/docshell/test/browser/browser_click_link_within_view_source.js new file mode 100644 index 0000000000..73542f977a --- /dev/null +++ b/docshell/test/browser/browser_click_link_within_view_source.js @@ -0,0 +1,78 @@ +"use strict"; + +/** + * Test for Bug 1359204 + * + * Loading a local file, then view-source on that file. Make sure that + * clicking a link within that view-source page is not blocked by security checks. + */ + +add_task(async function test_click_link_within_view_source() { + let TEST_FILE = "file_click_link_within_view_source.html"; + let TEST_FILE_URI = getChromeDir(getResolvedURI(gTestPath)); + TEST_FILE_URI.append(TEST_FILE); + TEST_FILE_URI = Services.io.newFileURI(TEST_FILE_URI).spec; + + let DUMMY_FILE = "dummy_page.html"; + let DUMMY_FILE_URI = getChromeDir(getResolvedURI(gTestPath)); + DUMMY_FILE_URI.append(DUMMY_FILE); + DUMMY_FILE_URI = Services.io.newFileURI(DUMMY_FILE_URI).spec; + + await BrowserTestUtils.withNewTab(TEST_FILE_URI, async function (aBrowser) { + let tabSpec = gBrowser.selectedBrowser.currentURI.spec; + info("loading: " + tabSpec); + ok( + tabSpec.startsWith("file://") && tabSpec.endsWith(TEST_FILE), + "sanity check to make sure html loaded" + ); + + info("click view-source of html"); + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + document.getElementById("View:PageSource").doCommand(); + + let tab = await tabPromise; + tabSpec = gBrowser.selectedBrowser.currentURI.spec; + info("loading: " + tabSpec); + ok( + tabSpec.startsWith("view-source:file://") && tabSpec.endsWith(TEST_FILE), + "loading view-source of html succeeded" + ); + + info("click testlink within view-source page"); + let loadPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + url => url.endsWith("dummy_page.html") + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + if (content.document.readyState != "complete") { + await ContentTaskUtils.waitForEvent( + content.document, + "readystatechange", + false, + () => content.document.readyState == "complete" + ); + } + // document.getElementById() does not work on a view-source page, hence we use document.links + let linksOnPage = content.document.links; + is( + linksOnPage.length, + 1, + "sanity check: make sure only one link is present on page" + ); + let myLink = content.document.links[0]; + myLink.click(); + }); + + await loadPromise; + + tabSpec = gBrowser.selectedBrowser.currentURI.spec; + info("loading: " + tabSpec); + ok( + tabSpec.startsWith("view-source:file://") && tabSpec.endsWith(DUMMY_FILE), + "loading view-source of html succeeded" + ); + + BrowserTestUtils.removeTab(tab); + }); +}); diff --git a/docshell/test/browser/browser_cross_process_csp_inheritance.js b/docshell/test/browser/browser_cross_process_csp_inheritance.js new file mode 100644 index 0000000000..76be61d0f9 --- /dev/null +++ b/docshell/test/browser/browser_cross_process_csp_inheritance.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" +); +const TEST_URI = TEST_PATH + "file_cross_process_csp_inheritance.html"; +const DATA_URI = + "data:text/html,<html>test-same-diff-process-csp-inhertiance</html>"; + +const FISSION_ENABLED = SpecialPowers.useRemoteSubframes; + +function getCurrentPID(aBrowser) { + return SpecialPowers.spawn(aBrowser, [], () => { + return Services.appinfo.processID; + }); +} + +function getCurrentURI(aBrowser) { + return SpecialPowers.spawn(aBrowser, [], () => { + let channel = content.docShell.currentDocumentChannel; + return channel.URI.asciiSpec; + }); +} + +function verifyResult( + aTestName, + aBrowser, + aDataURI, + aPID, + aSamePID, + aFissionEnabled +) { + return SpecialPowers.spawn( + aBrowser, + [{ aTestName, aDataURI, aPID, aSamePID, aFissionEnabled }], + async function ({ aTestName, aDataURI, aPID, aSamePID, aFissionEnabled }) { + // sanity, to make sure the correct URI was loaded + let channel = content.docShell.currentDocumentChannel; + is( + channel.URI.asciiSpec, + aDataURI, + aTestName + ": correct data uri loaded" + ); + + // check that the process ID is the same/different when opening the new tab + let pid = Services.appinfo.processID; + if (aSamePID) { + is(pid, aPID, aTestName + ": process ID needs to be identical"); + } else if (aFissionEnabled) { + // TODO: Fission discards dom.noopener.newprocess.enabled and puts + // data: URIs in the same process. Unfortunately todo_isnot is not + // defined in that scope, hence we have to use a workaround. + todo( + false, + pid == aPID, + ": process ID needs to be different in fission" + ); + } else { + isnot(pid, aPID, aTestName + ": process ID needs to be different"); + } + + // finally, evaluate that the CSP was set. + let cspOBJ = JSON.parse(content.document.cspJSON); + let policies = cspOBJ["csp-policies"]; + is(policies.length, 1, "should be one policy"); + let policy = policies[0]; + is( + policy["script-src"], + "'none'", + aTestName + ": script-src directive matches" + ); + } + ); +} + +async function simulateCspInheritanceForNewTab(aTestName, aSamePID) { + await BrowserTestUtils.withNewTab(TEST_URI, async function (browser) { + // do some sanity checks + let currentURI = await getCurrentURI(gBrowser.selectedBrowser); + is(currentURI, TEST_URI, aTestName + ": correct test uri loaded"); + + let pid = await getCurrentPID(gBrowser.selectedBrowser); + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI, true); + // simulate click + BrowserTestUtils.synthesizeMouseAtCenter( + "#testLink", + {}, + gBrowser.selectedBrowser + ); + let tab = await loadPromise; + gBrowser.selectTabAtIndex(2); + await verifyResult( + aTestName, + gBrowser.selectedBrowser, + DATA_URI, + pid, + aSamePID, + FISSION_ENABLED + ); + await BrowserTestUtils.removeTab(tab); + }); +} + +add_task(async function test_csp_inheritance_diff_process() { + // forcing the new data: URI load to happen in a *new* process by flipping the pref + // to force <a rel="noopener" ...> to be loaded in a new process. + await SpecialPowers.pushPrefEnv({ + set: [["dom.noopener.newprocess.enabled", true]], + }); + await simulateCspInheritanceForNewTab("diff-process-inheritance", false); +}); + +add_task(async function test_csp_inheritance_same_process() { + // forcing the new data: URI load to happen in a *same* process by resetting the pref + // and loaded <a rel="noopener" ...> in the *same* process. + await SpecialPowers.pushPrefEnv({ + set: [["dom.noopener.newprocess.enabled", false]], + }); + await simulateCspInheritanceForNewTab("same-process-inheritance", true); +}); diff --git a/docshell/test/browser/browser_csp_sandbox_no_script_js_uri.js b/docshell/test/browser/browser_csp_sandbox_no_script_js_uri.js new file mode 100644 index 0000000000..d0b92084ec --- /dev/null +++ b/docshell/test/browser/browser_csp_sandbox_no_script_js_uri.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +/** + * Test that javascript URIs in CSP-sandboxed contexts can't be used to bypass + * script restrictions. + */ +add_task(async function test_csp_sandbox_no_script_js_uri() { + await BrowserTestUtils.withNewTab( + TEST_PATH + "dummy_page.html", + async browser => { + info("Register observer and wait for javascript-uri-blocked message."); + let observerPromise = SpecialPowers.spawn(browser, [], () => { + return new Promise(resolve => { + SpecialPowers.addObserver(function obs(subject) { + ok( + subject == content, + "Should block script spawned via javascript uri" + ); + SpecialPowers.removeObserver( + obs, + "javascript-uri-blocked-by-sandbox" + ); + resolve(); + }, "javascript-uri-blocked-by-sandbox"); + }); + }); + + info("Spawn csp-sandboxed iframe with javascript URI"); + let frameBC = await SpecialPowers.spawn( + browser, + [TEST_PATH + "file_csp_sandbox_no_script_js_uri.html"], + async url => { + let frame = content.document.createElement("iframe"); + let loadPromise = ContentTaskUtils.waitForEvent(frame, "load", true); + frame.src = url; + content.document.body.appendChild(frame); + await loadPromise; + return frame.browsingContext; + } + ); + + info("Click javascript URI link in iframe"); + BrowserTestUtils.synthesizeMouseAtCenter("a", {}, frameBC); + await observerPromise; + } + ); +}); diff --git a/docshell/test/browser/browser_csp_uir.js b/docshell/test/browser/browser_csp_uir.js new file mode 100644 index 0000000000..2eb36d3d4d --- /dev/null +++ b/docshell/test/browser/browser_csp_uir.js @@ -0,0 +1,89 @@ +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" +); +const TEST_URI = TEST_PATH + "file_csp_uir.html"; // important to be http: to test upgrade-insecure-requests +const RESULT_URI = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + TEST_PATH.replace("http://", "https://") + "file_csp_uir_dummy.html"; + +function verifyCSP(aTestName, aBrowser, aResultURI) { + return SpecialPowers.spawn( + aBrowser, + [{ aTestName, aResultURI }], + async function ({ aTestName, aResultURI }) { + let channel = content.docShell.currentDocumentChannel; + is(channel.URI.asciiSpec, aResultURI, "testing CSP for " + aTestName); + } + ); +} + +add_task(async function test_csp_inheritance_regular_click() { + await BrowserTestUtils.withNewTab(TEST_URI, async function (browser) { + let loadPromise = BrowserTestUtils.browserLoaded( + browser, + false, + RESULT_URI + ); + // set the data href + simulate click + BrowserTestUtils.synthesizeMouseAtCenter( + "#testlink", + {}, + gBrowser.selectedBrowser + ); + await loadPromise; + await verifyCSP("click()", gBrowser.selectedBrowser, RESULT_URI); + }); +}); + +add_task(async function test_csp_inheritance_ctrl_click() { + await BrowserTestUtils.withNewTab(TEST_URI, async function (browser) { + let loadPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + RESULT_URI, + true + ); + // set the data href + simulate ctrl+click + BrowserTestUtils.synthesizeMouseAtCenter( + "#testlink", + { ctrlKey: true, metaKey: true }, + gBrowser.selectedBrowser + ); + let tab = await loadPromise; + gBrowser.selectTabAtIndex(2); + await verifyCSP("ctrl-click()", gBrowser.selectedBrowser, RESULT_URI); + await BrowserTestUtils.removeTab(tab); + }); +}); + +add_task( + async function test_csp_inheritance_right_click_open_link_in_new_tab() { + await BrowserTestUtils.withNewTab(TEST_URI, async function (browser) { + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, RESULT_URI); + // set the data href + simulate right-click open link in tab + BrowserTestUtils.waitForEvent(document, "popupshown", false, event => { + // These are operations that must be executed synchronously with the event. + document.getElementById("context-openlinkintab").doCommand(); + event.target.hidePopup(); + return true; + }); + BrowserTestUtils.synthesizeMouseAtCenter( + "#testlink", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + + let tab = await loadPromise; + gBrowser.selectTabAtIndex(2); + await verifyCSP( + "right-click-open-in-new-tab()", + gBrowser.selectedBrowser, + RESULT_URI + ); + await BrowserTestUtils.removeTab(tab); + }); + } +); diff --git a/docshell/test/browser/browser_dataURI_unique_opaque_origin.js b/docshell/test/browser/browser_dataURI_unique_opaque_origin.js new file mode 100644 index 0000000000..c46012fa82 --- /dev/null +++ b/docshell/test/browser/browser_dataURI_unique_opaque_origin.js @@ -0,0 +1,30 @@ +add_task(async function test_dataURI_unique_opaque_origin() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + let pagePrincipal = browser.contentPrincipal; + info("pagePrincial " + pagePrincipal.origin); + + BrowserTestUtils.loadURIString(browser, "data:text/html,hi"); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ principal: pagePrincipal }], + async function (args) { + info("data URI principal: " + content.document.nodePrincipal.origin); + Assert.ok( + content.document.nodePrincipal.isNullPrincipal, + "data: URI should have NullPrincipal." + ); + Assert.ok( + !content.document.nodePrincipal.equals(args.principal), + "data: URI should have unique opaque origin." + ); + } + ); + + gBrowser.removeTab(tab); +}); diff --git a/docshell/test/browser/browser_data_load_inherit_csp.js b/docshell/test/browser/browser_data_load_inherit_csp.js new file mode 100644 index 0000000000..b2bc86e0ea --- /dev/null +++ b/docshell/test/browser/browser_data_load_inherit_csp.js @@ -0,0 +1,110 @@ +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" +); +const HTML_URI = TEST_PATH + "file_data_load_inherit_csp.html"; +const DATA_URI = "data:text/html;html,<html><body>foo</body></html>"; + +function setDataHrefOnLink(aBrowser, aDataURI) { + return SpecialPowers.spawn(aBrowser, [aDataURI], function (uri) { + let link = content.document.getElementById("testlink"); + link.href = uri; + }); +} + +function verifyCSP(aTestName, aBrowser, aDataURI) { + return SpecialPowers.spawn( + aBrowser, + [{ aTestName, aDataURI }], + async function ({ aTestName, aDataURI }) { + let channel = content.docShell.currentDocumentChannel; + is(channel.URI.spec, aDataURI, "testing CSP for " + aTestName); + let cspJSON = content.document.cspJSON; + let cspOBJ = JSON.parse(cspJSON); + let policies = cspOBJ["csp-policies"]; + is(policies.length, 1, "should be one policy"); + let policy = policies[0]; + is( + policy["script-src"], + "'unsafe-inline'", + "script-src directive matches" + ); + } + ); +} + +add_setup(async function () { + // allow top level data: URI navigations, otherwise clicking data: link fails + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", false]], + }); +}); + +add_task(async function test_data_csp_inheritance_regular_click() { + await BrowserTestUtils.withNewTab(HTML_URI, async function (browser) { + let loadPromise = BrowserTestUtils.browserLoaded(browser, false, DATA_URI); + // set the data href + simulate click + await setDataHrefOnLink(gBrowser.selectedBrowser, DATA_URI); + BrowserTestUtils.synthesizeMouseAtCenter( + "#testlink", + {}, + gBrowser.selectedBrowser + ); + await loadPromise; + await verifyCSP("click()", gBrowser.selectedBrowser, DATA_URI); + }); +}); + +add_task(async function test_data_csp_inheritance_ctrl_click() { + await BrowserTestUtils.withNewTab(HTML_URI, async function (browser) { + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI, true); + // set the data href + simulate ctrl+click + await setDataHrefOnLink(gBrowser.selectedBrowser, DATA_URI); + BrowserTestUtils.synthesizeMouseAtCenter( + "#testlink", + { ctrlKey: true, metaKey: true }, + gBrowser.selectedBrowser + ); + let tab = await loadPromise; + gBrowser.selectTabAtIndex(2); + await verifyCSP("ctrl-click()", gBrowser.selectedBrowser, DATA_URI); + await BrowserTestUtils.removeTab(tab); + }); +}); + +add_task( + async function test_data_csp_inheritance_right_click_open_link_in_new_tab() { + await BrowserTestUtils.withNewTab(HTML_URI, async function (browser) { + let loadPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + DATA_URI, + true + ); + // set the data href + simulate right-click open link in tab + await setDataHrefOnLink(gBrowser.selectedBrowser, DATA_URI); + BrowserTestUtils.waitForEvent(document, "popupshown", false, event => { + // These are operations that must be executed synchronously with the event. + document.getElementById("context-openlinkintab").doCommand(); + event.target.hidePopup(); + return true; + }); + BrowserTestUtils.synthesizeMouseAtCenter( + "#testlink", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + + let tab = await loadPromise; + gBrowser.selectTabAtIndex(2); + await verifyCSP( + "right-click-open-in-new-tab()", + gBrowser.selectedBrowser, + DATA_URI + ); + await BrowserTestUtils.removeTab(tab); + }); + } +); diff --git a/docshell/test/browser/browser_fall_back_to_https.js b/docshell/test/browser/browser_fall_back_to_https.js new file mode 100644 index 0000000000..b84780eec1 --- /dev/null +++ b/docshell/test/browser/browser_fall_back_to_https.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * This test is for bug 1002724. + * https://bugzilla.mozilla.org/show_bug.cgi?id=1002724 + * + * When a user enters a host name or IP address in the URL bar, "http" is + * assumed. If the host rejects connections on port 80, we try HTTPS as a + * fall-back and only fail if HTTPS connection fails. + * + * This tests that when a user enters "example.com", it attempts to load + * http://example.com:80 (not rejected), and when trying secureonly.example.com + * (which rejects connections on port 80), it fails then loads + * https://secureonly.example.com:443 instead. + */ + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +const bug1002724_tests = [ + { + original: "example.com", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + expected: "http://example.com", + explanation: "Should load HTTP version of example.com", + }, + { + original: "secureonly.example.com", + expected: "https://secureonly.example.com", + explanation: + "Should reject secureonly.example.com on HTTP but load the HTTPS version", + }, +]; + +async function test_one(test_obj) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + gURLBar.focus(); + gURLBar.value = test_obj.original; + + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + ok( + tab.linkedBrowser.currentURI.spec.startsWith(test_obj.expected), + test_obj.explanation + ); + + BrowserTestUtils.removeTab(tab); +} + +add_task(async function test_bug1002724() { + await SpecialPowers.pushPrefEnv( + // Disable HSTS preload just in case. + { + set: [ + ["network.stricttransportsecurity.preloadlist", false], + ["network.dns.native-is-localhost", true], + ], + } + ); + + for (let test of bug1002724_tests) { + await test_one(test); + } +}); diff --git a/docshell/test/browser/browser_frameloader_swap_with_bfcache.js b/docshell/test/browser/browser_frameloader_swap_with_bfcache.js new file mode 100644 index 0000000000..2f7d494845 --- /dev/null +++ b/docshell/test/browser/browser_frameloader_swap_with_bfcache.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +add_task(async function test() { + if ( + !SpecialPowers.Services.appinfo.sessionHistoryInParent || + !SpecialPowers.Services.prefs.getBoolPref("fission.bfcacheInParent") + ) { + ok( + true, + "This test is currently only for the bfcache in the parent process." + ); + return; + } + const uri = + "data:text/html,<script>onpageshow = function(e) { document.documentElement.setAttribute('persisted', e.persisted); }; </script>"; + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri, true); + let browser1 = tab1.linkedBrowser; + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri, true); + let browser2 = tab2.linkedBrowser; + BrowserTestUtils.loadURIString(browser2, uri + "nextpage"); + await BrowserTestUtils.browserLoaded(browser2, false); + + gBrowser.swapBrowsersAndCloseOther(tab1, tab2); + is(tab1.linkedBrowser, browser1, "Tab's browser should stay the same."); + browser1.goBack(false); + await BrowserTestUtils.browserLoaded(browser1, false); + let persisted = await SpecialPowers.spawn(browser1, [], async function () { + return content.document.documentElement.getAttribute("persisted"); + }); + + is(persisted, "false", "BFCache should be evicted when swapping browsers."); + + BrowserTestUtils.removeTab(tab1); +}); diff --git a/docshell/test/browser/browser_history_triggeringprincipal_viewsource.js b/docshell/test/browser/browser_history_triggeringprincipal_viewsource.js new file mode 100644 index 0000000000..d542ef892c --- /dev/null +++ b/docshell/test/browser/browser_history_triggeringprincipal_viewsource.js @@ -0,0 +1,88 @@ +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" +); +const HTML_URI = TEST_PATH + "dummy_page.html"; +const VIEW_SRC_URI = "view-source:" + HTML_URI; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + + info("load baseline html in new tab"); + await BrowserTestUtils.withNewTab(HTML_URI, async function (aBrowser) { + is( + gBrowser.selectedBrowser.currentURI.spec, + HTML_URI, + "sanity check to make sure html loaded" + ); + + info("right-click -> view-source of html"); + let vSrcCtxtMenu = document.getElementById("contentAreaContextMenu"); + let popupPromise = BrowserTestUtils.waitForEvent( + vSrcCtxtMenu, + "popupshown" + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "body", + { type: "contextmenu", button: 2 }, + aBrowser + ); + await popupPromise; + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, VIEW_SRC_URI); + let vSrcItem = vSrcCtxtMenu.querySelector("#context-viewsource"); + vSrcCtxtMenu.activateItem(vSrcItem); + let tab = await tabPromise; + is( + gBrowser.selectedBrowser.currentURI.spec, + VIEW_SRC_URI, + "loading view-source of html succeeded" + ); + + info("load html file again before going .back()"); + let loadPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + HTML_URI + ); + await SpecialPowers.spawn(tab.linkedBrowser, [HTML_URI], HTML_URI => { + content.document.location = HTML_URI; + }); + await loadPromise; + is( + gBrowser.selectedBrowser.currentURI.spec, + HTML_URI, + "loading html another time succeeded" + ); + + info( + "click .back() to view-source of html again and make sure the history entry has a triggeringPrincipal" + ); + let backCtxtMenu = document.getElementById("contentAreaContextMenu"); + popupPromise = BrowserTestUtils.waitForEvent(backCtxtMenu, "popupshown"); + BrowserTestUtils.synthesizeMouseAtCenter( + "body", + { type: "contextmenu", button: 2 }, + aBrowser + ); + await popupPromise; + loadPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + let backItem = backCtxtMenu.querySelector("#context-back"); + backCtxtMenu.activateItem(backItem); + await loadPromise; + is( + gBrowser.selectedBrowser.currentURI.spec, + VIEW_SRC_URI, + "clicking .back() to view-source of html succeeded" + ); + + BrowserTestUtils.removeTab(tab); + }); +}); diff --git a/docshell/test/browser/browser_isInitialDocument.js b/docshell/test/browser/browser_isInitialDocument.js new file mode 100644 index 0000000000..7a047c61d0 --- /dev/null +++ b/docshell/test/browser/browser_isInitialDocument.js @@ -0,0 +1,319 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tag every new WindowGlobalParent with an expando indicating whether or not +// they were an initial document when they were created for the duration of this +// test. +function wasInitialDocumentObserver(subject) { + subject._test_wasInitialDocument = subject.isInitialDocument; +} +Services.obs.addObserver(wasInitialDocumentObserver, "window-global-created"); +SimpleTest.registerCleanupFunction(function () { + Services.obs.removeObserver( + wasInitialDocumentObserver, + "window-global-created" + ); +}); + +add_task(async function new_about_blank_tab() { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + is( + browser.browsingContext.currentWindowGlobal.isInitialDocument, + false, + "After loading an actual, final about:blank in the tab, the field is false" + ); + }); +}); + +add_task(async function iframe_initial_about_blank() { + await BrowserTestUtils.withNewTab( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/document-builder.sjs?html=com", + async browser => { + info("Create an iframe without any explicit location"); + await SpecialPowers.spawn(browser, [], async () => { + const iframe = content.document.createElement("iframe"); + // Add the iframe to the DOM tree in order to be able to have its browsingContext + content.document.body.appendChild(iframe); + const { browsingContext } = iframe; + + is( + iframe.contentDocument.isInitialDocument, + true, + "The field is true on just-created iframes" + ); + let beforeLoadPromise = SpecialPowers.spawnChrome( + [browsingContext], + bc => [ + bc.currentWindowGlobal.isInitialDocument, + bc.currentWindowGlobal._test_wasInitialDocument, + ] + ); + + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + is( + iframe.contentDocument.isInitialDocument, + false, + "The field is false after having loaded the final about:blank document" + ); + let afterLoadPromise = SpecialPowers.spawnChrome( + [browsingContext], + bc => [ + bc.currentWindowGlobal.isInitialDocument, + bc.currentWindowGlobal._test_wasInitialDocument, + ] + ); + + // Wait to await the parent process promises, so we can't miss the "load" event. + let [beforeIsInitial, beforeWasInitial] = await beforeLoadPromise; + is(beforeIsInitial, true, "before load is initial in parent"); + is(beforeWasInitial, true, "before load was initial in parent"); + let [afterIsInitial, afterWasInitial] = await afterLoadPromise; + is(afterIsInitial, false, "after load is not initial in parent"); + is(afterWasInitial, true, "after load was initial in parent"); + iframe.remove(); + }); + + info("Create an iframe with a cross origin location"); + const iframeBC = await SpecialPowers.spawn(browser, [], async () => { + const iframe = content.document.createElement("iframe"); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + iframe.src = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/document-builder.sjs?html=org-iframe"; + content.document.body.appendChild(iframe); + }); + + return iframe.browsingContext; + }); + + is( + iframeBC.currentWindowGlobal.isInitialDocument, + false, + "The field is true after having loaded the final document" + ); + } + ); +}); + +add_task(async function window_open() { + async function testWindowOpen({ browser, args, isCrossOrigin, willLoad }) { + info(`Open popup with ${JSON.stringify(args)}`); + const onNewTab = BrowserTestUtils.waitForNewTab( + gBrowser, + args[0] || "about:blank" + ); + await SpecialPowers.spawn( + browser, + [args, isCrossOrigin, willLoad], + async (args, crossOrigin, willLoad) => { + const win = content.window.open(...args); + is( + win.document.isInitialDocument, + true, + "The field is true right after calling window.open()" + ); + let beforeLoadPromise = SpecialPowers.spawnChrome( + [win.browsingContext], + bc => [ + bc.currentWindowGlobal.isInitialDocument, + bc.currentWindowGlobal._test_wasInitialDocument, + ] + ); + + // In cross origin, it is harder to watch for new document load, and if + // no argument is passed no load will happen. + if (!crossOrigin && willLoad) { + await new Promise(r => + win.addEventListener("load", r, { once: true }) + ); + is( + win.document.isInitialDocument, + false, + "The field becomes false right after the popup document is loaded" + ); + } + + // Perform the await after the load to avoid missing it. + let [beforeIsInitial, beforeWasInitial] = await beforeLoadPromise; + is(beforeIsInitial, true, "before load is initial in parent"); + is(beforeWasInitial, true, "before load was initial in parent"); + } + ); + const newTab = await onNewTab; + const windowGlobal = + newTab.linkedBrowser.browsingContext.currentWindowGlobal; + if (willLoad) { + is( + windowGlobal.isInitialDocument, + false, + "The field is false in the parent process after having loaded the final document" + ); + } else { + is( + windowGlobal.isInitialDocument, + true, + "The field remains true in the parent process as nothing will be loaded" + ); + } + BrowserTestUtils.removeTab(newTab); + } + + await BrowserTestUtils.withNewTab( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/document-builder.sjs?html=com", + async browser => { + info("Use window.open() with cross-origin document"); + await testWindowOpen({ + browser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + args: ["http://example.org/document-builder.sjs?html=org-popup"], + isCrossOrigin: true, + willLoad: true, + }); + + info("Use window.open() with same-origin document"); + await testWindowOpen({ + browser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + args: ["http://example.com/document-builder.sjs?html=com-popup"], + isCrossOrigin: false, + willLoad: true, + }); + + info("Use window.open() with final about:blank document"); + await testWindowOpen({ + browser, + args: ["about:blank"], + isCrossOrigin: false, + willLoad: true, + }); + + info("Use window.open() with no argument"); + await testWindowOpen({ + browser, + args: [], + isCrossOrigin: false, + willLoad: false, + }); + } + ); +}); + +add_task(async function document_open() { + await BrowserTestUtils.withNewTab( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/document-builder.sjs?html=com", + async browser => { + is(browser.browsingContext.currentWindowGlobal.isInitialDocument, false); + await SpecialPowers.spawn(browser, [], async () => { + const iframe = content.document.createElement("iframe"); + // Add the iframe to the DOM tree in order to be able to have its browsingContext + content.document.body.appendChild(iframe); + const { browsingContext } = iframe; + + // Check the state before the call in both parent and content. + is( + iframe.contentDocument.isInitialDocument, + true, + "Is an initial document before calling document.open" + ); + let beforeOpenParentPromise = SpecialPowers.spawnChrome( + [browsingContext], + bc => [ + bc.currentWindowGlobal.isInitialDocument, + bc.currentWindowGlobal._test_wasInitialDocument, + bc.currentWindowGlobal.innerWindowId, + ] + ); + + // Run the `document.open` call with reduced permissions. + iframe.contentWindow.eval(` + document.open(); + document.write("new document"); + document.close(); + `); + + is( + iframe.contentDocument.isInitialDocument, + false, + "Is no longer an initial document after calling document.open" + ); + let [afterIsInitial, afterWasInitial, afterID] = + await SpecialPowers.spawnChrome([browsingContext], bc => [ + bc.currentWindowGlobal.isInitialDocument, + bc.currentWindowGlobal._test_wasInitialDocument, + bc.currentWindowGlobal.innerWindowId, + ]); + let [beforeIsInitial, beforeWasInitial, beforeID] = + await beforeOpenParentPromise; + is(beforeIsInitial, true, "Should be initial before in the parent"); + is(beforeWasInitial, true, "Was initial before in the parent"); + is(afterIsInitial, false, "Should not be initial after in the parent"); + is(afterWasInitial, true, "Was initial after in the parent"); + is(beforeID, afterID, "Should be the same WindowGlobalParent"); + }); + } + ); +}); + +add_task(async function windowless_browser() { + info("Create a Windowless browser"); + const browser = Services.appShell.createWindowlessBrowser(false); + const { browsingContext } = browser; + is( + browsingContext.currentWindowGlobal.isInitialDocument, + true, + "The field is true for a freshly created WindowlessBrowser" + ); + is( + browser.currentURI.spec, + "about:blank", + "The location is immediately set to about:blank" + ); + + const principal = Services.scriptSecurityManager.getSystemPrincipal(); + browser.docShell.createAboutBlankContentViewer(principal, principal); + is( + browsingContext.currentWindowGlobal.isInitialDocument, + false, + "The field becomes false when creating an artificial blank document" + ); + + info("Load a final about:blank document in it"); + const onLocationChange = new Promise(resolve => { + let wpl = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + onLocationChange() { + browsingContext.webProgress.removeProgressListener( + wpl, + Ci.nsIWebProgress.NOTIFY_ALL + ); + resolve(); + }, + }; + browsingContext.webProgress.addProgressListener( + wpl, + Ci.nsIWebProgress.NOTIFY_ALL + ); + }); + browser.loadURI(Services.io.newURI("about:blank"), { + triggeringPrincipal: principal, + }); + info("Wait for the location change"); + await onLocationChange; + is( + browsingContext.currentWindowGlobal.isInitialDocument, + false, + "The field is false after the location change event" + ); + browser.close(); +}); diff --git a/docshell/test/browser/browser_loadURI_postdata.js b/docshell/test/browser/browser_loadURI_postdata.js new file mode 100644 index 0000000000..b2dbb7a641 --- /dev/null +++ b/docshell/test/browser/browser_loadURI_postdata.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const gPostData = "postdata=true"; +const gUrl = + "http://mochi.test:8888/browser/docshell/test/browser/print_postdata.sjs"; + +add_task(async function test_loadURI_persists_postData() { + waitForExplicitFinish(); + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + }); + + var dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + dataStream.data = gPostData; + + var postStream = Cc[ + "@mozilla.org/network/mime-input-stream;1" + ].createInstance(Ci.nsIMIMEInputStream); + postStream.addHeader("Content-Type", "application/x-www-form-urlencoded"); + postStream.setData(dataStream); + var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].getService( + Ci.nsIPrincipal + ); + + tab.linkedBrowser.loadURI(Services.io.newURI(gUrl), { + triggeringPrincipal: systemPrincipal, + postData: postStream, + }); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, gUrl); + let body = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => content.document.body.textContent + ); + is(body, gPostData, "post data was submitted correctly"); + finish(); +}); diff --git a/docshell/test/browser/browser_multiple_pushState.js b/docshell/test/browser/browser_multiple_pushState.js new file mode 100644 index 0000000000..5d07747543 --- /dev/null +++ b/docshell/test/browser/browser_multiple_pushState.js @@ -0,0 +1,25 @@ +add_task(async function test_multiple_pushState() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/browser/docshell/test/browser/file_multiple_pushState.html", + }, + async function (browser) { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const kExpected = "http://example.org/bar/ABC/DEF?key=baz"; + + let contentLocation = await SpecialPowers.spawn( + browser, + [], + async function () { + return content.document.location.href; + } + ); + + is(contentLocation, kExpected); + is(browser.documentURI.spec, kExpected); + } + ); +}); diff --git a/docshell/test/browser/browser_onbeforeunload_frame.js b/docshell/test/browser/browser_onbeforeunload_frame.js new file mode 100644 index 0000000000..d697af718d --- /dev/null +++ b/docshell/test/browser/browser_onbeforeunload_frame.js @@ -0,0 +1,45 @@ +"use strict"; + +// We need to test a lot of permutations here, and there isn't any sensible way +// to split them up or run them faster. +requestLongerTimeout(12); + +Services.scriptloader.loadSubScript( + getRootDirectory(gTestPath) + "head_browser_onbeforeunload.js", + this +); + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); + + for (let actions of PERMUTATIONS) { + info( + `Testing frame actions: [${actions.map(action => + ACTION_NAMES.get(action) + )}]` + ); + + for (let startIdx = 0; startIdx < FRAMES.length; startIdx++) { + info(`Testing content reload from frame ${startIdx}`); + + await doTest(actions, startIdx, (tab, frames) => { + return SpecialPowers.spawn(frames[startIdx], [], () => { + let eventLoopSpun = false; + SpecialPowers.Services.tm.dispatchToMainThread(() => { + eventLoopSpun = true; + }); + + content.location.reload(); + + return { eventLoopSpun }; + }); + }); + } + } +}); + +add_task(async function cleanup() { + await TabPool.cleanup(); +}); diff --git a/docshell/test/browser/browser_onbeforeunload_navigation.js b/docshell/test/browser/browser_onbeforeunload_navigation.js new file mode 100644 index 0000000000..23dc3b528e --- /dev/null +++ b/docshell/test/browser/browser_onbeforeunload_navigation.js @@ -0,0 +1,165 @@ +"use strict"; + +const TEST_PAGE = + "http://mochi.test:8888/browser/docshell/test/browser/file_bug1046022.html"; +const TARGETED_PAGE = + "data:text/html," + + encodeURIComponent("<body>Shouldn't be seeing this</body>"); + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +var loadStarted = false; +var tabStateListener = { + resolveLoad: null, + expectLoad: null, + + onStateChange(webprogress, request, flags, status) { + const WPL = Ci.nsIWebProgressListener; + if (flags & WPL.STATE_IS_WINDOW) { + if (flags & WPL.STATE_START) { + loadStarted = true; + } else if (flags & WPL.STATE_STOP) { + let url = request.QueryInterface(Ci.nsIChannel).URI.spec; + is(url, this.expectLoad, "Should only see expected document loads"); + if (url == this.expectLoad) { + this.resolveLoad(); + } + } + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +function promiseLoaded(url, callback) { + if (tabStateListener.expectLoad) { + throw new Error("Can't wait for multiple loads at once"); + } + tabStateListener.expectLoad = url; + return new Promise(resolve => { + tabStateListener.resolveLoad = resolve; + if (callback) { + callback(); + } + }).then(() => { + tabStateListener.expectLoad = null; + tabStateListener.resolveLoad = null; + }); +} + +function promiseStayOnPagePrompt(browser, acceptNavigation) { + return PromptTestUtils.handleNextPrompt( + browser, + { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" }, + { buttonNumClick: acceptNavigation ? 0 : 1 } + ); +} + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); + + let testTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE, + false, + true + ); + let browser = testTab.linkedBrowser; + browser.addProgressListener( + tabStateListener, + Ci.nsIWebProgress.NOTIFY_STATE_WINDOW + ); + + const NUM_TESTS = 7; + await SpecialPowers.spawn(browser, [NUM_TESTS], testCount => { + let { testFns } = this.content.wrappedJSObject; + Assert.equal( + testFns.length, + testCount, + "Should have the correct number of test functions" + ); + }); + + for (let allowNavigation of [false, true]) { + for (let i = 0; i < NUM_TESTS; i++) { + info( + `Running test ${i} with navigation ${ + allowNavigation ? "allowed" : "forbidden" + }` + ); + + if (allowNavigation) { + // If we're allowing navigations, we need to re-load the test + // page after each test, since the tests will each navigate away + // from it. + await promiseLoaded(TEST_PAGE, () => { + browser.loadURI(Services.io.newURI(TEST_PAGE), { + triggeringPrincipal: document.nodePrincipal, + }); + }); + } + + let promptPromise = promiseStayOnPagePrompt(browser, allowNavigation); + let loadPromise; + if (allowNavigation) { + loadPromise = promiseLoaded(TARGETED_PAGE); + } + + let winID = await SpecialPowers.spawn( + browser, + [i, TARGETED_PAGE], + (testIdx, url) => { + let { testFns } = this.content.wrappedJSObject; + this.content.onbeforeunload = testFns[testIdx]; + this.content.location = url; + return this.content.windowGlobalChild.innerWindowId; + } + ); + + await promptPromise; + await loadPromise; + + if (allowNavigation) { + await SpecialPowers.spawn( + browser, + [TARGETED_PAGE, winID], + (url, winID) => { + this.content.onbeforeunload = null; + Assert.equal( + this.content.location.href, + url, + "Page should have navigated to the correct URL" + ); + Assert.notEqual( + this.content.windowGlobalChild.innerWindowId, + winID, + "Page should have a new inner window" + ); + } + ); + } else { + await SpecialPowers.spawn(browser, [TEST_PAGE, winID], (url, winID) => { + this.content.onbeforeunload = null; + Assert.equal( + this.content.location.href, + url, + "Page should have the same URL" + ); + Assert.equal( + this.content.windowGlobalChild.innerWindowId, + winID, + "Page should have the same inner window" + ); + }); + } + } + } + + gBrowser.removeTab(testTab); +}); diff --git a/docshell/test/browser/browser_onbeforeunload_parent.js b/docshell/test/browser/browser_onbeforeunload_parent.js new file mode 100644 index 0000000000..72c5004334 --- /dev/null +++ b/docshell/test/browser/browser_onbeforeunload_parent.js @@ -0,0 +1,48 @@ +"use strict"; + +// We need to test a lot of permutations here, and there isn't any sensible way +// to split them up or run them faster. +requestLongerTimeout(6); + +Services.scriptloader.loadSubScript( + getRootDirectory(gTestPath) + "head_browser_onbeforeunload.js", + this +); + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); + + for (let actions of PERMUTATIONS) { + info( + `Testing frame actions: [${actions.map(action => + ACTION_NAMES.get(action) + )}]` + ); + + info(`Testing tab close from parent process`); + await doTest(actions, -1, (tab, frames) => { + let eventLoopSpun = false; + Services.tm.dispatchToMainThread(() => { + eventLoopSpun = true; + }); + + BrowserTestUtils.removeTab(tab); + + let result = { eventLoopSpun }; + + // Make an extra couple of trips through the event loop to give us time + // to process SpecialPowers.spawn responses before resolving. + return new Promise(resolve => { + executeSoon(() => { + executeSoon(() => resolve(result)); + }); + }); + }); + } +}); + +add_task(async function cleanup() { + await TabPool.cleanup(); +}); diff --git a/docshell/test/browser/browser_onunload_stop.js b/docshell/test/browser/browser_onunload_stop.js new file mode 100644 index 0000000000..24b2c6aab7 --- /dev/null +++ b/docshell/test/browser/browser_onunload_stop.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PAGE_1 = + "http://mochi.test:8888/browser/docshell/test/browser/dummy_page.html"; + +const TEST_PAGE_2 = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/browser/docshell/test/browser/dummy_page.html"; + +add_task(async function test() { + await BrowserTestUtils.withNewTab(TEST_PAGE_1, async function (browser) { + let loaded = BrowserTestUtils.browserLoaded(browser, false, TEST_PAGE_2); + await SpecialPowers.spawn(browser, [], () => { + content.addEventListener("unload", e => e.currentTarget.stop(), true); + }); + BrowserTestUtils.loadURIString(browser, TEST_PAGE_2); + await loaded; + ok(true, "Page loaded successfully"); + }); +}); diff --git a/docshell/test/browser/browser_overlink.js b/docshell/test/browser/browser_overlink.js new file mode 100644 index 0000000000..0bd88f697a --- /dev/null +++ b/docshell/test/browser/browser_overlink.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(async function test_stripAuthCredentials() { + await BrowserTestUtils.withNewTab( + TEST_PATH + "overlink_test.html", + async function (browser) { + await SpecialPowers.spawn(browser, [], function () { + content.document.getElementById("link").focus(); + }); + + await TestUtils.waitForCondition( + () => XULBrowserWindow.overLink == "https://example.com", + "Overlink should be missing auth credentials" + ); + + ok(true, "Test successful"); + } + ); +}); diff --git a/docshell/test/browser/browser_platform_emulation.js b/docshell/test/browser/browser_platform_emulation.js new file mode 100644 index 0000000000..017b615e15 --- /dev/null +++ b/docshell/test/browser/browser_platform_emulation.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = "data:text/html;charset=utf-8,<iframe id='test-iframe'></iframe>"; + +async function contentTaskNoOverride() { + let docshell = docShell; + is( + docshell.browsingContext.customPlatform, + "", + "There should initially be no customPlatform" + ); +} + +async function contentTaskOverride() { + let docshell = docShell; + is( + docshell.browsingContext.customPlatform, + "foo", + "The platform should be changed to foo" + ); + + is( + content.navigator.platform, + "foo", + "The platform should be changed to foo" + ); + + let frameWin = content.document.querySelector("#test-iframe").contentWindow; + is( + frameWin.navigator.platform, + "foo", + "The platform should be passed on to frames." + ); + + let newFrame = content.document.createElement("iframe"); + content.document.body.appendChild(newFrame); + + let newFrameWin = newFrame.contentWindow; + is( + newFrameWin.navigator.platform, + "foo", + "Newly created frames should use the new platform" + ); + + newFrameWin.location.reload(); + await ContentTaskUtils.waitForEvent(newFrame, "load"); + + is( + newFrameWin.navigator.platform, + "foo", + "New platform should persist across reloads" + ); +} + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + await SpecialPowers.spawn(browser, [], contentTaskNoOverride); + + let browsingContext = BrowserTestUtils.getBrowsingContextFrom(browser); + browsingContext.customPlatform = "foo"; + + await SpecialPowers.spawn(browser, [], contentTaskOverride); + } + ); +}); diff --git a/docshell/test/browser/browser_search_notification.js b/docshell/test/browser/browser_search_notification.js new file mode 100644 index 0000000000..f98ad22a40 --- /dev/null +++ b/docshell/test/browser/browser_search_notification.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +SearchTestUtils.init(this); + +add_task(async function () { + // Our search would be handled by the urlbar normally and not by the docshell, + // thus we must force going through dns first, so that the urlbar thinks + // the value may be a url, and asks the docshell to visit it. + // On NS_ERROR_UNKNOWN_HOST the docshell will fix it up. + await SpecialPowers.pushPrefEnv({ + set: [["browser.fixup.dns_first_for_single_words", true]], + }); + const kSearchEngineID = "test_urifixup_search_engine"; + await SearchTestUtils.installSearchExtension( + { + name: kSearchEngineID, + search_url: "http://localhost/", + search_url_get_params: "search={searchTerms}", + }, + { setAsDefault: true } + ); + + let selectedName = (await Services.search.getDefault()).name; + Assert.equal( + selectedName, + kSearchEngineID, + "Check fake search engine is selected" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gBrowser.selectedTab = tab; + + gURLBar.value = "firefox"; + gURLBar.handleCommand(); + + let [subject, data] = await TestUtils.topicObserved("keyword-search"); + + let engine = subject.QueryInterface(Ci.nsISupportsString).data; + + Assert.equal(engine, kSearchEngineID, "Should be the search engine id"); + Assert.equal(data, "firefox", "Notification data is search term."); + + gBrowser.removeTab(tab); +}); diff --git a/docshell/test/browser/browser_tab_replace_while_loading.js b/docshell/test/browser/browser_tab_replace_while_loading.js new file mode 100644 index 0000000000..3c85f72192 --- /dev/null +++ b/docshell/test/browser/browser_tab_replace_while_loading.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* Test for bug 1578379. */ + +add_task(async function test_window_open_about_blank() { + const URL = + "http://mochi.test:8888/browser/docshell/test/browser/file_open_about_blank.html"; + let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:blank" + ); + + info("Opening about:blank using a click"); + await SpecialPowers.spawn(firstTab.linkedBrowser, [""], async function () { + content.document.querySelector("#open").click(); + }); + + info("Waiting for the second tab to be opened"); + let secondTab = await promiseTabOpened; + + info("Detaching tab"); + let windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + gBrowser.replaceTabWithWindow(secondTab); + let win = await windowOpenedPromise; + + info("Asserting document is visible"); + let tab = win.gBrowser.selectedTab; + await SpecialPowers.spawn(tab.linkedBrowser, [""], async function () { + is( + content.document.visibilityState, + "visible", + "Document should be visible" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await BrowserTestUtils.removeTab(firstTab); +}); + +add_task(async function test_detach_loading_page() { + const URL = + "http://mochi.test:8888/browser/docshell/test/browser/file_slow_load.sjs"; + // Open a dummy tab so that detaching the second tab works. + let dummyTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + let slowLoadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + URL, + /* waitForLoad = */ false + ); + + info("Wait for content document to be created"); + await BrowserTestUtils.waitForCondition(async function () { + return SpecialPowers.spawn( + slowLoadingTab.linkedBrowser, + [URL], + async function (url) { + return content.document.documentURI == url; + } + ); + }); + + info("Detaching tab"); + let windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + gBrowser.replaceTabWithWindow(slowLoadingTab); + let win = await windowOpenedPromise; + + info("Asserting document is visible"); + let tab = win.gBrowser.selectedTab; + await SpecialPowers.spawn(tab.linkedBrowser, [""], async function () { + is(content.document.readyState, "loading"); + is(content.document.visibilityState, "visible"); + }); + + await BrowserTestUtils.closeWindow(win); + await BrowserTestUtils.removeTab(dummyTab); +}); diff --git a/docshell/test/browser/browser_tab_touch_events.js b/docshell/test/browser/browser_tab_touch_events.js new file mode 100644 index 0000000000..c73b01e45c --- /dev/null +++ b/docshell/test/browser/browser_tab_touch_events.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + const URI = "data:text/html;charset=utf-8,<iframe id='test-iframe'></iframe>"; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: URI }, + async function (browser) { + await SpecialPowers.spawn(browser, [], test_init); + + browser.browsingContext.touchEventsOverride = "disabled"; + + await SpecialPowers.spawn(browser, [], test_body); + } + ); +}); + +async function test_init() { + is( + content.browsingContext.touchEventsOverride, + "none", + "touchEventsOverride flag should be initially set to NONE" + ); +} + +async function test_body() { + let bc = content.browsingContext; + is( + bc.touchEventsOverride, + "disabled", + "touchEventsOverride flag should be changed to DISABLED" + ); + + let frameWin = content.document.querySelector("#test-iframe").contentWindow; + bc = frameWin.browsingContext; + is( + bc.touchEventsOverride, + "disabled", + "touchEventsOverride flag should be passed on to frames." + ); + + let newFrame = content.document.createElement("iframe"); + content.document.body.appendChild(newFrame); + + let newFrameWin = newFrame.contentWindow; + bc = newFrameWin.browsingContext; + is( + bc.touchEventsOverride, + "disabled", + "Newly created frames should use the new touchEventsOverride flag" + ); + + // Wait for the non-transient about:blank to load. + await ContentTaskUtils.waitForEvent(newFrame, "load"); + newFrameWin = newFrame.contentWindow; + bc = newFrameWin.browsingContext; + is( + bc.touchEventsOverride, + "disabled", + "Newly created frames should use the new touchEventsOverride flag" + ); + + newFrameWin.location.reload(); + await ContentTaskUtils.waitForEvent(newFrame, "load"); + newFrameWin = newFrame.contentWindow; + bc = newFrameWin.browsingContext; + is( + bc.touchEventsOverride, + "disabled", + "New touchEventsOverride flag should persist across reloads" + ); +} diff --git a/docshell/test/browser/browser_targetTopLevelLinkClicksToBlank.js b/docshell/test/browser/browser_targetTopLevelLinkClicksToBlank.js new file mode 100644 index 0000000000..9035750b93 --- /dev/null +++ b/docshell/test/browser/browser_targetTopLevelLinkClicksToBlank.js @@ -0,0 +1,285 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test exercises the behaviour where user-initiated link clicks on + * the top-level document result in pageloads in a _blank target in a new + * browser window. + */ + +const TEST_PAGE = "https://example.com/browser/"; +const TEST_PAGE_2 = "https://example.com/browser/components/"; +const TEST_IFRAME_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "dummy_iframe_page.html"; + +// There is an <a> element with this href=".." in the TEST_PAGE +// that we will click, which should take us up a level. +const LINK_URL = "https://example.com/"; + +/** + * Test that a user-initiated link click results in targeting to a new + * <browser> element, and that this properly sets the referrer on the newly + * loaded document. + */ +add_task(async function target_to_new_blank_browser() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + let originalTab = win.gBrowser.selectedTab; + let originalBrowser = originalTab.linkedBrowser; + BrowserTestUtils.loadURIString(originalBrowser, TEST_PAGE); + await BrowserTestUtils.browserLoaded(originalBrowser, false, TEST_PAGE); + + // Now set the targetTopLevelLinkClicksToBlank property to true, since it + // defaults to false. + originalBrowser.browsingContext.targetTopLevelLinkClicksToBlank = true; + + let newTabPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, LINK_URL); + await SpecialPowers.spawn(originalBrowser, [], async () => { + let anchor = content.document.querySelector(`a[href=".."]`); + let userInput = content.windowUtils.setHandlingUserInput(true); + try { + anchor.click(); + } finally { + userInput.destruct(); + } + }); + let newTab = await newTabPromise; + let newBrowser = newTab.linkedBrowser; + + Assert.ok( + originalBrowser !== newBrowser, + "A new browser should have been created." + ); + await SpecialPowers.spawn(newBrowser, [TEST_PAGE], async referrer => { + Assert.equal( + content.document.referrer, + referrer, + "Should have gotten the right referrer set" + ); + }); + await BrowserTestUtils.switchTab(win.gBrowser, originalTab); + BrowserTestUtils.removeTab(newTab); + + // Now do the same thing with a subframe targeting "_top". This should also + // get redirected to "_blank". + BrowserTestUtils.loadURIString(originalBrowser, TEST_IFRAME_PAGE); + await BrowserTestUtils.browserLoaded( + originalBrowser, + false, + TEST_IFRAME_PAGE + ); + + newTabPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, LINK_URL); + let frameBC1 = originalBrowser.browsingContext.children[0]; + Assert.ok(frameBC1, "Should have found a subframe BrowsingContext"); + + await SpecialPowers.spawn(frameBC1, [LINK_URL], async linkUrl => { + let anchor = content.document.createElement("a"); + anchor.setAttribute("href", linkUrl); + anchor.setAttribute("target", "_top"); + content.document.body.appendChild(anchor); + let userInput = content.windowUtils.setHandlingUserInput(true); + try { + anchor.click(); + } finally { + userInput.destruct(); + } + }); + newTab = await newTabPromise; + newBrowser = newTab.linkedBrowser; + + Assert.ok( + originalBrowser !== newBrowser, + "A new browser should have been created." + ); + await SpecialPowers.spawn( + newBrowser, + [frameBC1.currentURI.spec], + async referrer => { + Assert.equal( + content.document.referrer, + referrer, + "Should have gotten the right referrer set" + ); + } + ); + await BrowserTestUtils.switchTab(win.gBrowser, originalTab); + BrowserTestUtils.removeTab(newTab); + + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Test that we don't target to _blank loads caused by: + * 1. POST requests + * 2. Any load that isn't "normal" (in the nsIDocShell.LOAD_CMD_NORMAL sense) + * 3. Any loads that are caused by location.replace + * 4. Any loads that were caused by setting location.href + * 5. Link clicks fired without user interaction. + */ +add_task(async function skip_blank_target_for_some_loads() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + let currentBrowser = win.gBrowser.selectedBrowser; + BrowserTestUtils.loadURIString(currentBrowser, TEST_PAGE); + await BrowserTestUtils.browserLoaded(currentBrowser, false, TEST_PAGE); + + // Now set the targetTopLevelLinkClicksToBlank property to true, since it + // defaults to false. + currentBrowser.browsingContext.targetTopLevelLinkClicksToBlank = true; + + let ensureSingleBrowser = () => { + Assert.equal( + win.gBrowser.browsers.length, + 1, + "There should only be 1 browser." + ); + + Assert.ok( + currentBrowser.browsingContext.targetTopLevelLinkClicksToBlank, + "Should still be targeting top-level clicks to _blank" + ); + }; + + // First we'll test a POST request + let sameBrowserLoad = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + TEST_PAGE + ); + await SpecialPowers.spawn(currentBrowser, [], async () => { + let doc = content.document; + let form = doc.createElement("form"); + form.setAttribute("method", "post"); + doc.body.appendChild(form); + let userInput = content.windowUtils.setHandlingUserInput(true); + try { + form.submit(); + } finally { + userInput.destruct(); + } + }); + await sameBrowserLoad; + ensureSingleBrowser(); + + // Next, we'll try a non-normal load - specifically, we'll try a reload. + // Since we've got a page loaded via a POST request, an attempt to reload + // will cause the "repost" dialog to appear, so we temporarily allow the + // repost to go through with the always_accept testing pref. + await SpecialPowers.pushPrefEnv({ + set: [["dom.confirm_repost.testing.always_accept", true]], + }); + sameBrowserLoad = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + TEST_PAGE + ); + await SpecialPowers.spawn(currentBrowser, [], async () => { + let userInput = content.windowUtils.setHandlingUserInput(true); + try { + content.location.reload(); + } finally { + userInput.destruct(); + } + }); + await sameBrowserLoad; + ensureSingleBrowser(); + await SpecialPowers.popPrefEnv(); + + // Next, we'll try a location.replace + sameBrowserLoad = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + TEST_PAGE_2 + ); + await SpecialPowers.spawn(currentBrowser, [TEST_PAGE_2], async page2 => { + let userInput = content.windowUtils.setHandlingUserInput(true); + try { + content.location.replace(page2); + } finally { + userInput.destruct(); + } + }); + await sameBrowserLoad; + ensureSingleBrowser(); + + // Finally we'll try setting location.href + sameBrowserLoad = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + TEST_PAGE + ); + await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async page1 => { + let userInput = content.windowUtils.setHandlingUserInput(true); + try { + content.location.href = page1; + } finally { + userInput.destruct(); + } + }); + await sameBrowserLoad; + ensureSingleBrowser(); + + // Now that we're back at TEST_PAGE, let's try a scripted link click. This + // shouldn't target to _blank. + sameBrowserLoad = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + LINK_URL + ); + await SpecialPowers.spawn(currentBrowser, [], async () => { + let anchor = content.document.querySelector(`a[href=".."]`); + anchor.click(); + }); + await sameBrowserLoad; + ensureSingleBrowser(); + + // A javascript:void(0); link should also not target to _blank. + sameBrowserLoad = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + TEST_PAGE + ); + await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async newPageURL => { + let anchor = content.document.querySelector(`a[href=".."]`); + anchor.href = "javascript:void(0);"; + anchor.addEventListener("click", e => { + content.location.href = newPageURL; + }); + let userInput = content.windowUtils.setHandlingUserInput(true); + try { + anchor.click(); + } finally { + userInput.destruct(); + } + }); + await sameBrowserLoad; + ensureSingleBrowser(); + + // Let's also try a non-void javascript: location. + sameBrowserLoad = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + TEST_PAGE + ); + await SpecialPowers.spawn(currentBrowser, [TEST_PAGE], async newPageURL => { + let anchor = content.document.querySelector(`a[href=".."]`); + anchor.href = `javascript:"string-to-navigate-to"`; + anchor.addEventListener("click", e => { + content.location.href = newPageURL; + }); + let userInput = content.windowUtils.setHandlingUserInput(true); + try { + anchor.click(); + } finally { + userInput.destruct(); + } + }); + await sameBrowserLoad; + ensureSingleBrowser(); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/docshell/test/browser/browser_timelineMarkers-01.js b/docshell/test/browser/browser_timelineMarkers-01.js new file mode 100644 index 0000000000..7dd51a14fc --- /dev/null +++ b/docshell/test/browser/browser_timelineMarkers-01.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the docShell has the right profile timeline API + +const URL = "data:text/html;charset=utf-8,Test page"; + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + await SpecialPowers.spawn(browser, [], function () { + ok( + "recordProfileTimelineMarkers" in docShell, + "The recordProfileTimelineMarkers attribute exists" + ); + ok( + "popProfileTimelineMarkers" in docShell, + "The popProfileTimelineMarkers function exists" + ); + ok( + docShell.recordProfileTimelineMarkers === false, + "recordProfileTimelineMarkers is false by default" + ); + ok( + docShell.popProfileTimelineMarkers().length === 0, + "There are no markers by default" + ); + + docShell.recordProfileTimelineMarkers = true; + ok( + docShell.recordProfileTimelineMarkers === true, + "recordProfileTimelineMarkers can be set to true" + ); + + docShell.recordProfileTimelineMarkers = false; + ok( + docShell.recordProfileTimelineMarkers === false, + "recordProfileTimelineMarkers can be set to false" + ); + }); + } + ); +}); diff --git a/docshell/test/browser/browser_timelineMarkers-02.js b/docshell/test/browser/browser_timelineMarkers-02.js new file mode 100644 index 0000000000..a2b569d9d6 --- /dev/null +++ b/docshell/test/browser/browser_timelineMarkers-02.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var TEST_URL = + "<!DOCTYPE html><style>" + + "body {margin:0; padding: 0;} " + + "div {width:100px;height:100px;background:red;} " + + ".resize-change-color {width:50px;height:50px;background:blue;} " + + ".change-color {width:50px;height:50px;background:yellow;} " + + ".add-class {}" + + "</style><div></div>"; +TEST_URL = "data:text/html;charset=utf8," + encodeURIComponent(TEST_URL); + +var test = makeTimelineTest("browser_timelineMarkers-frame-02.js", TEST_URL); diff --git a/docshell/test/browser/browser_timelineMarkers-frame-02.js b/docshell/test/browser/browser_timelineMarkers-frame-02.js new file mode 100644 index 0000000000..52d1e43782 --- /dev/null +++ b/docshell/test/browser/browser_timelineMarkers-frame-02.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/frame-script */ + +// This file expects frame-head.js to be loaded in the environment. +/* import-globals-from frame-head.js */ + +"use strict"; + +// Test that the docShell profile timeline API returns the right markers when +// restyles, reflows and paints occur + +function rectangleContains(rect, x, y, width, height) { + return ( + rect.x <= x && rect.y <= y && rect.width >= width && rect.height >= height + ); +} + +function sanitizeMarkers(list) { + // These markers are currently gathered from all docshells, which may + // interfere with this test. + return list.filter(e => e.name != "Worker" && e.name != "MinorGC"); +} + +var TESTS = [ + { + desc: "Changing the width of the test element", + searchFor: "Paint", + setup(docShell) { + let div = content.document.querySelector("div"); + div.setAttribute("class", "resize-change-color"); + }, + check(markers) { + markers = sanitizeMarkers(markers); + ok(!!markers.length, "markers were returned"); + console.log(markers); + info(JSON.stringify(markers.filter(m => m.name == "Paint"))); + ok( + markers.some(m => m.name == "Reflow"), + "markers includes Reflow" + ); + ok( + markers.some(m => m.name == "Paint"), + "markers includes Paint" + ); + for (let marker of markers.filter(m => m.name == "Paint")) { + // This change should generate at least one rectangle. + ok(marker.rectangles.length >= 1, "marker has one rectangle"); + // One of the rectangles should contain the div. + ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 100, 100))); + } + ok( + markers.some(m => m.name == "Styles"), + "markers includes Restyle" + ); + }, + }, + { + desc: "Changing the test element's background color", + searchFor: "Paint", + setup(docShell) { + let div = content.document.querySelector("div"); + div.setAttribute("class", "change-color"); + }, + check(markers) { + markers = sanitizeMarkers(markers); + ok(!!markers.length, "markers were returned"); + ok( + !markers.some(m => m.name == "Reflow"), + "markers doesn't include Reflow" + ); + ok( + markers.some(m => m.name == "Paint"), + "markers includes Paint" + ); + for (let marker of markers.filter(m => m.name == "Paint")) { + // This change should generate at least one rectangle. + ok(marker.rectangles.length >= 1, "marker has one rectangle"); + // One of the rectangles should contain the div. + ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 50, 50))); + } + ok( + markers.some(m => m.name == "Styles"), + "markers includes Restyle" + ); + }, + }, + { + desc: "Changing the test element's classname", + searchFor: "Paint", + setup(docShell) { + let div = content.document.querySelector("div"); + div.setAttribute("class", "change-color add-class"); + }, + check(markers) { + markers = sanitizeMarkers(markers); + ok(!!markers.length, "markers were returned"); + ok( + !markers.some(m => m.name == "Reflow"), + "markers doesn't include Reflow" + ); + ok( + !markers.some(m => m.name == "Paint"), + "markers doesn't include Paint" + ); + ok( + markers.some(m => m.name == "Styles"), + "markers includes Restyle" + ); + }, + }, + { + desc: "sync console.time/timeEnd", + searchFor: "ConsoleTime", + setup(docShell) { + content.console.time("FOOBAR"); + content.console.timeEnd("FOOBAR"); + let markers = docShell.popProfileTimelineMarkers(); + is(markers.length, 1, "Got one marker"); + is(markers[0].name, "ConsoleTime", "Got ConsoleTime marker"); + is(markers[0].causeName, "FOOBAR", "Got ConsoleTime FOOBAR detail"); + content.console.time("FOO"); + content.setTimeout(() => { + content.console.time("BAR"); + content.setTimeout(() => { + content.console.timeEnd("FOO"); + content.console.timeEnd("BAR"); + }, 100); + }, 100); + }, + check(markers) { + markers = sanitizeMarkers(markers); + is(markers.length, 2, "Got 2 markers"); + is(markers[0].name, "ConsoleTime", "Got first ConsoleTime marker"); + is(markers[0].causeName, "FOO", "Got ConsoleTime FOO detail"); + is(markers[1].name, "ConsoleTime", "Got second ConsoleTime marker"); + is(markers[1].causeName, "BAR", "Got ConsoleTime BAR detail"); + }, + }, + { + desc: "Timestamps created by console.timeStamp()", + searchFor: "Timestamp", + setup(docShell) { + content.console.timeStamp("rock"); + let markers = docShell.popProfileTimelineMarkers(); + is(markers.length, 1, "Got one marker"); + is(markers[0].name, "TimeStamp", "Got Timestamp marker"); + is(markers[0].causeName, "rock", "Got Timestamp label value"); + content.console.timeStamp("paper"); + content.console.timeStamp("scissors"); + content.console.timeStamp(); + content.console.timeStamp(undefined); + }, + check(markers) { + markers = sanitizeMarkers(markers); + is(markers.length, 4, "Got 4 markers"); + is(markers[0].name, "TimeStamp", "Got Timestamp marker"); + is(markers[0].causeName, "paper", "Got Timestamp label value"); + is(markers[1].name, "TimeStamp", "Got Timestamp marker"); + is(markers[1].causeName, "scissors", "Got Timestamp label value"); + is( + markers[2].name, + "TimeStamp", + "Got empty Timestamp marker when no argument given" + ); + is(markers[2].causeName, void 0, "Got empty Timestamp label value"); + is( + markers[3].name, + "TimeStamp", + "Got empty Timestamp marker when argument is undefined" + ); + is(markers[3].causeName, void 0, "Got empty Timestamp label value"); + markers.forEach(m => + is( + m.end, + m.start, + "All Timestamp markers should have identical start/end times" + ) + ); + }, + }, +]; + +timelineContentTest(TESTS); diff --git a/docshell/test/browser/browser_title_in_session_history.js b/docshell/test/browser/browser_title_in_session_history.js new file mode 100644 index 0000000000..bdcbbb7dfe --- /dev/null +++ b/docshell/test/browser/browser_title_in_session_history.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test() { + const TEST_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "dummy_page.html"; + await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => { + let titles = await ContentTask.spawn(browser, null, () => { + return new Promise(resolve => { + let titles = []; + content.document.body.innerHTML = "<div id='foo'>foo</div>"; + content.document.title = "Initial"; + content.history.pushState("1", "1", "1"); + content.document.title = "1"; + content.history.pushState("2", "2", "2"); + content.document.title = "2"; + content.location.hash = "hash"; + content.document.title = "3-hash"; + content.addEventListener( + "popstate", + () => { + content.addEventListener( + "popstate", + () => { + titles.push(content.document.title); + resolve(titles); + }, + { once: true } + ); + + titles.push(content.document.title); + // Test going forward a few steps. + content.history.go(2); + }, + { once: true } + ); + // Test going back a few steps. + content.history.go(-3); + }); + }); + is( + titles[0], + "3-hash", + "Document.title should have the value to which it was last time set." + ); + is( + titles[1], + "3-hash", + "Document.title should have the value to which it was last time set." + ); + let sh = browser.browsingContext.sessionHistory; + let count = sh.count; + is(sh.getEntryAtIndex(count - 1).title, "3-hash"); + is(sh.getEntryAtIndex(count - 2).title, "2"); + is(sh.getEntryAtIndex(count - 3).title, "1"); + is(sh.getEntryAtIndex(count - 4).title, "Initial"); + }); +}); diff --git a/docshell/test/browser/browser_ua_emulation.js b/docshell/test/browser/browser_ua_emulation.js new file mode 100644 index 0000000000..f1e9657cd3 --- /dev/null +++ b/docshell/test/browser/browser_ua_emulation.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = "data:text/html;charset=utf-8,<iframe id='test-iframe'></iframe>"; + +// Test that the docShell UA emulation works +async function contentTaskNoOverride() { + let docshell = docShell; + is( + docshell.browsingContext.customUserAgent, + "", + "There should initially be no customUserAgent" + ); +} + +async function contentTaskOverride() { + let docshell = docShell; + is( + docshell.browsingContext.customUserAgent, + "foo", + "The user agent should be changed to foo" + ); + + is( + content.navigator.userAgent, + "foo", + "The user agent should be changed to foo" + ); + + let frameWin = content.document.querySelector("#test-iframe").contentWindow; + is( + frameWin.navigator.userAgent, + "foo", + "The UA should be passed on to frames." + ); + + let newFrame = content.document.createElement("iframe"); + content.document.body.appendChild(newFrame); + + let newFrameWin = newFrame.contentWindow; + is( + newFrameWin.navigator.userAgent, + "foo", + "Newly created frames should use the new UA" + ); + + newFrameWin.location.reload(); + await ContentTaskUtils.waitForEvent(newFrame, "load"); + + is( + newFrameWin.navigator.userAgent, + "foo", + "New UA should persist across reloads" + ); +} + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + await SpecialPowers.spawn(browser, [], contentTaskNoOverride); + + let browsingContext = BrowserTestUtils.getBrowsingContextFrom(browser); + browsingContext.customUserAgent = "foo"; + + await SpecialPowers.spawn(browser, [], contentTaskOverride); + } + ); +}); diff --git a/docshell/test/browser/browser_uriFixupAlternateRedirects.js b/docshell/test/browser/browser_uriFixupAlternateRedirects.js new file mode 100644 index 0000000000..193e8cf489 --- /dev/null +++ b/docshell/test/browser/browser_uriFixupAlternateRedirects.js @@ -0,0 +1,66 @@ +"use strict"; + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +const REDIRECTURL = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://www.example.com/browser/docshell/test/browser/redirect_to_example.sjs"; + +add_task(async function () { + // Test both directly setting a value and pressing enter, or setting the + // value through input events, like the user would do. + const setValueFns = [ + value => { + gURLBar.value = value; + }, + value => { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value, + }); + }, + ]; + for (let setValueFn of setValueFns) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + // Enter search terms and start a search. + gURLBar.focus(); + await setValueFn(REDIRECTURL); + let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await errorPageLoaded; + let [contentURL, originalURL] = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return [ + content.document.documentURI, + content.document.mozDocumentURIIfNotForErrorPages.spec, + ]; + } + ); + info("Page that loaded: " + contentURL); + const errorURI = "about:neterror?"; + ok(contentURL.startsWith(errorURI), "Should be on an error page"); + + const contentPrincipal = tab.linkedBrowser.contentPrincipal; + ok( + contentPrincipal.spec.startsWith(errorURI), + "Principal should be for the error page" + ); + + originalURL = new URL(originalURL); + is( + originalURL.host, + "example", + "Should be an error for http://example, not http://www.example.com/" + ); + + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/docshell/test/browser/browser_uriFixupIntegration.js b/docshell/test/browser/browser_uriFixupIntegration.js new file mode 100644 index 0000000000..619fd9f61f --- /dev/null +++ b/docshell/test/browser/browser_uriFixupIntegration.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +SearchTestUtils.init(this); + +const kSearchEngineID = "browser_urifixup_search_engine"; +const kSearchEngineURL = "https://example.com/?search={searchTerms}"; +const kPrivateSearchEngineID = "browser_urifixup_search_engine_private"; +const kPrivateSearchEngineURL = "https://example.com/?private={searchTerms}"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", true], + ], + }); + + // Add new fake search engines. + await SearchTestUtils.installSearchExtension( + { + name: kSearchEngineID, + search_url: "https://example.com/", + search_url_get_params: "search={searchTerms}", + }, + { setAsDefault: true } + ); + + await SearchTestUtils.installSearchExtension( + { + name: kPrivateSearchEngineID, + search_url: "https://example.com/", + search_url_get_params: "private={searchTerms}", + }, + { setAsDefaultPrivate: true } + ); +}); + +add_task(async function test() { + // Test both directly setting a value and pressing enter, or setting the + // value through input events, like the user would do. + const setValueFns = [ + (value, win) => { + win.gURLBar.value = value; + }, + (value, win) => { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + waitForFocus: SimpleTest.waitForFocus, + value, + }); + }, + ]; + + for (let value of ["foo bar", "brokenprotocol:somethingelse"]) { + for (let setValueFn of setValueFns) { + for (let inPrivateWindow of [false, true]) { + await do_test(value, setValueFn, inPrivateWindow); + } + } + } +}); + +async function do_test(value, setValueFn, inPrivateWindow) { + info(`Search ${value} in a ${inPrivateWindow ? "private" : "normal"} window`); + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: inPrivateWindow, + }); + // Enter search terms and start a search. + win.gURLBar.focus(); + await setValueFn(value, win); + + EventUtils.synthesizeKey("KEY_Enter", {}, win); + + // Check that we load the correct URL. + let escapedValue = encodeURIComponent(value).replace("%20", "+"); + let searchEngineUrl = inPrivateWindow + ? kPrivateSearchEngineURL + : kSearchEngineURL; + let expectedURL = searchEngineUrl.replace("{searchTerms}", escapedValue); + await BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser, + false, + expectedURL + ); + // There should be at least one test. + Assert.equal( + win.gBrowser.selectedBrowser.currentURI.spec, + expectedURL, + "New tab should have loaded with expected url." + ); + + // Cleanup. + await BrowserTestUtils.closeWindow(win); +} diff --git a/docshell/test/browser/browser_viewsource_chrome_to_content.js b/docshell/test/browser/browser_viewsource_chrome_to_content.js new file mode 100644 index 0000000000..680041d704 --- /dev/null +++ b/docshell/test/browser/browser_viewsource_chrome_to_content.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_URI = `view-source:${TEST_PATH}dummy_page.html`; + +add_task(async function chrome_to_content_view_source() { + await BrowserTestUtils.withNewTab("about:mozilla", async browser => { + is(browser.documentURI.spec, "about:mozilla"); + + // This process switch would previously crash in debug builds due to assertion failures. + BrowserTestUtils.loadURIString(browser, TEST_URI); + await BrowserTestUtils.browserLoaded(browser); + is(browser.documentURI.spec, TEST_URI); + }); +}); diff --git a/docshell/test/browser/browser_viewsource_multipart.js b/docshell/test/browser/browser_viewsource_multipart.js new file mode 100644 index 0000000000..0a902a71ec --- /dev/null +++ b/docshell/test/browser/browser_viewsource_multipart.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const MULTIPART_URI = `${TEST_PATH}file_basic_multipart.sjs`; + +add_task(async function viewsource_multipart_uri() { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + BrowserTestUtils.loadURIString(browser, MULTIPART_URI); + await BrowserTestUtils.browserLoaded(browser); + is(browser.currentURI.spec, MULTIPART_URI); + + // Continue probing the URL until we find the h1 we're expecting. This + // should handle cases where we somehow beat the second document having + // loaded. + await TestUtils.waitForCondition(async () => { + let value = await SpecialPowers.spawn(browser, [], async () => { + let headers = content.document.querySelectorAll("h1"); + is(headers.length, 1, "only one h1 should be present"); + return headers[0].textContent; + }); + + ok(value == "First" || value == "Second", "some other value was found?"); + return value == "Second"; + }); + + // Load a view-source version of the page, which should show the full + // content, not handling multipart. + BrowserTestUtils.loadURIString(browser, `view-source:${MULTIPART_URI}`); + await BrowserTestUtils.browserLoaded(browser); + + let viewSourceContent = await SpecialPowers.spawn(browser, [], async () => { + return content.document.body.textContent; + }); + + ok(viewSourceContent.includes("<h1>First</h1>"), "first header"); + ok(viewSourceContent.includes("<h1>Second</h1>"), "second header"); + ok(viewSourceContent.includes("BOUNDARY"), "boundary"); + }); +}); diff --git a/docshell/test/browser/dummy_iframe_page.html b/docshell/test/browser/dummy_iframe_page.html new file mode 100644 index 0000000000..12ce921856 --- /dev/null +++ b/docshell/test/browser/dummy_iframe_page.html @@ -0,0 +1,8 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + just a dummy html file with an iframe + <iframe id="frame1" src="dummy_page.html?sub_entry=0"></iframe> + <iframe id="frame2" src="dummy_page.html?sub_entry=0"></iframe> + </body> +</html> diff --git a/docshell/test/browser/dummy_page.html b/docshell/test/browser/dummy_page.html new file mode 100644 index 0000000000..59bf2a5f8f --- /dev/null +++ b/docshell/test/browser/dummy_page.html @@ -0,0 +1,6 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + just a dummy html file + </body> +</html> diff --git a/docshell/test/browser/favicon_bug655270.ico b/docshell/test/browser/favicon_bug655270.ico Binary files differnew file mode 100644 index 0000000000..d44438903b --- /dev/null +++ b/docshell/test/browser/favicon_bug655270.ico diff --git a/docshell/test/browser/file_backforward_restore_scroll.html b/docshell/test/browser/file_backforward_restore_scroll.html new file mode 100644 index 0000000000..5a40b36c10 --- /dev/null +++ b/docshell/test/browser/file_backforward_restore_scroll.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <iframe src="http://mochi.test:8888/"></iframe> + <iframe src="http://example.com/"></iframe> +</body> +</html> diff --git a/docshell/test/browser/file_backforward_restore_scroll.html^headers^ b/docshell/test/browser/file_backforward_restore_scroll.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/browser/file_backforward_restore_scroll.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/browser/file_basic_multipart.sjs b/docshell/test/browser/file_basic_multipart.sjs new file mode 100644 index 0000000000..5e89b93948 --- /dev/null +++ b/docshell/test/browser/file_basic_multipart.sjs @@ -0,0 +1,24 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader( + "Content-Type", + "multipart/x-mixed-replace;boundary=BOUNDARY", + false + ); + response.setStatusLine(request.httpVersion, 200, "OK"); + + response.write(`--BOUNDARY +Content-Type: text/html + +<h1>First</h1> +Will be replaced +--BOUNDARY +Content-Type: text/html + +<h1>Second</h1> +This will stick around +--BOUNDARY +--BOUNDARY-- +`); +} diff --git a/docshell/test/browser/file_bug1046022.html b/docshell/test/browser/file_bug1046022.html new file mode 100644 index 0000000000..27a1e1f079 --- /dev/null +++ b/docshell/test/browser/file_bug1046022.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Bug 1046022 - test navigating inside onbeforeunload</title> + </head> + <body> + Waiting for onbeforeunload to hit... + </body> + + <script> +var testFns = [ + function(e) { + e.target.location.href = "otherpage-href-set.html"; + return "stop"; + }, + function(e) { + e.target.location.reload(); + return "stop"; + }, + function(e) { + e.currentTarget.stop(); + return "stop"; + }, + function(e) { + e.target.location.replace("otherpage-location-replaced.html"); + return "stop"; + }, + function(e) { + var link = e.target.createElement("a"); + link.href = "otherpage.html"; + e.target.body.appendChild(link); + link.click(); + return "stop"; + }, + function(e) { + var link = e.target.createElement("a"); + link.href = "otherpage.html"; + link.setAttribute("target", "_blank"); + e.target.body.appendChild(link); + link.click(); + return "stop"; + }, + function(e) { + var link = e.target.createElement("a"); + link.href = e.target.location.href; + e.target.body.appendChild(link); + link.setAttribute("target", "somearbitrarywindow"); + link.click(); + return "stop"; + }, +]; + </script> +</html> diff --git a/docshell/test/browser/file_bug1206879.html b/docshell/test/browser/file_bug1206879.html new file mode 100644 index 0000000000..5313902a9b --- /dev/null +++ b/docshell/test/browser/file_bug1206879.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test page for bug 1206879</title> + </head> + <body> + <iframe src="http://example.com/"></iframe> + </body> +</html> diff --git a/docshell/test/browser/file_bug1328501.html b/docshell/test/browser/file_bug1328501.html new file mode 100644 index 0000000000..517ef53e02 --- /dev/null +++ b/docshell/test/browser/file_bug1328501.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Page with iframes</title> + <script type="application/javascript"> + let promiseResolvers = { + "testFrame1": {}, + "testFrame2": {}, + }; + let promises = [ + new Promise(r => promiseResolvers.testFrame1.resolve = r), + new Promise(r => promiseResolvers.testFrame2.resolve = r), + ]; + function frameLoaded(frame) { + promiseResolvers[frame].resolve(); + } + Promise.all(promises).then(() => window.dispatchEvent(new Event("frames-loaded"))); + </script> + </head> + <body onunload=""> + <div> + <iframe id="testFrame1" src="dummy_page.html" onload="frameLoaded(this.id);" ></iframe> + <iframe id="testFrame2" src="dummy_page.html" onload="frameLoaded(this.id);" ></iframe> + </div> + </body> +</html> diff --git a/docshell/test/browser/file_bug1328501_frame.html b/docshell/test/browser/file_bug1328501_frame.html new file mode 100644 index 0000000000..156dd41eaa --- /dev/null +++ b/docshell/test/browser/file_bug1328501_frame.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<html lang="en"> + <body>Subframe page for testing</body> +</html> diff --git a/docshell/test/browser/file_bug1328501_framescript.js b/docshell/test/browser/file_bug1328501_framescript.js new file mode 100644 index 0000000000..19c86c75e7 --- /dev/null +++ b/docshell/test/browser/file_bug1328501_framescript.js @@ -0,0 +1,38 @@ +// Forward iframe loaded event. + +/* eslint-env mozilla/frame-script */ + +addEventListener( + "frames-loaded", + e => sendAsyncMessage("test:frames-loaded"), + true, + true +); + +let requestObserver = { + observe(subject, topic, data) { + if (topic == "http-on-opening-request") { + // Get DOMWindow on all child docshells to force about:blank + // content viewers being created. + getChildDocShells().map(ds => { + ds + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsILoadContext).associatedWindow; + }); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), +}; +Services.obs.addObserver(requestObserver, "http-on-opening-request"); +addEventListener("unload", e => { + if (e.target == this) { + Services.obs.removeObserver(requestObserver, "http-on-opening-request"); + } +}); + +function getChildDocShells() { + return docShell.getAllDocShellsInSubtree( + Ci.nsIDocShellTreeItem.typeAll, + Ci.nsIDocShell.ENUMERATE_FORWARDS + ); +} diff --git a/docshell/test/browser/file_bug1543077-3-child.html b/docshell/test/browser/file_bug1543077-3-child.html new file mode 100644 index 0000000000..858a4623ed --- /dev/null +++ b/docshell/test/browser/file_bug1543077-3-child.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>No encoding declaration in parent or child</title> +</head> +<body> +<p>Hiragana letter a if decoded as ISO-2022-JP: $B$"(B</p> +</body> +</html> + diff --git a/docshell/test/browser/file_bug1543077-3.html b/docshell/test/browser/file_bug1543077-3.html new file mode 100644 index 0000000000..c4f467dd3f --- /dev/null +++ b/docshell/test/browser/file_bug1543077-3.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>No encoding declaration in parent or child</title> +</head> +<body> +<h1>No encoding declaration in parent or child</h1> + +<p>Hiragana letter a if decoded as ISO-2022-JP: $B$"(B</p> + +<iframe src="file_bug1543077-3-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug1622420.html b/docshell/test/browser/file_bug1622420.html new file mode 100644 index 0000000000..63beb38302 --- /dev/null +++ b/docshell/test/browser/file_bug1622420.html @@ -0,0 +1 @@ +<iframe src="http://example.com/"></iframe> diff --git a/docshell/test/browser/file_bug1648464-1-child.html b/docshell/test/browser/file_bug1648464-1-child.html new file mode 100644 index 0000000000..7bb1ad965b --- /dev/null +++ b/docshell/test/browser/file_bug1648464-1-child.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset=windows-1252> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>windows-1252 in parent and child, actually EUC-JP</title> +</head> +<body> +<p>Hiragana letter a if decoded as EUC-JP: </p> +<p>ʸ¸Ǥ</p> +</body> +</html> + diff --git a/docshell/test/browser/file_bug1648464-1.html b/docshell/test/browser/file_bug1648464-1.html new file mode 100644 index 0000000000..2051cf61ed --- /dev/null +++ b/docshell/test/browser/file_bug1648464-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset=windows-1252> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>windows-1252 in parent and child, actually EUC-JP</title> +</head> +<body> +<h1>windows-1252 in parent and child, actually EUC-JP</h1> + +<p>Hiragana letter a if decoded as EUC-JP: </p> +<p>ʸ¸Ǥ</p> + +<iframe src="file_bug1648464-1-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug1673702.json b/docshell/test/browser/file_bug1673702.json new file mode 100644 index 0000000000..3f562dd675 --- /dev/null +++ b/docshell/test/browser/file_bug1673702.json @@ -0,0 +1 @@ +{ "version": 1 } diff --git a/docshell/test/browser/file_bug1673702.json^headers^ b/docshell/test/browser/file_bug1673702.json^headers^ new file mode 100644 index 0000000000..6010bfd188 --- /dev/null +++ b/docshell/test/browser/file_bug1673702.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json; charset=utf-8 diff --git a/docshell/test/browser/file_bug1688368-1.sjs b/docshell/test/browser/file_bug1688368-1.sjs new file mode 100644 index 0000000000..0693b7970c --- /dev/null +++ b/docshell/test/browser/file_bug1688368-1.sjs @@ -0,0 +1,44 @@ +"use strict"; + +const DELAY = 1 * 1000; // Delay one second before completing the request. + +let nsTimer = Components.Constructor( + "@mozilla.org/timer;1", + "nsITimer", + "initWithCallback" +); + +let timer; + +function handleRequest(request, response) { + response.processAsync(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + response.write(`<!DOCTYPE html> +<html> +<head> + <title>UTF-8 file, 1024 bytes long!</title> +</head> +<body>`); + + // Note: We need to store a reference to the timer to prevent it from being + // canceled when it's GCed. + timer = new nsTimer( + () => { + var snowmen = + "\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083\u00E2\u0098\u0083"; + response.write( + snowmen + + ` +</body> +</html> + +` + ); + response.finish(); + }, + DELAY, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/docshell/test/browser/file_bug1691153.html b/docshell/test/browser/file_bug1691153.html new file mode 100644 index 0000000000..dea144eb41 --- /dev/null +++ b/docshell/test/browser/file_bug1691153.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>bug 1691153</title> +</head> +<body> +<h1>bug 1691153</h1> +<script> +function toBlobURL(data, mimeType) { + return URL.createObjectURL( + new Blob([data], { + type: mimeType, + }) + ); +} +// closing script element literal split up to not end the parent script element +let testurl = toBlobURL("<body></body>", "text/html"); +addEventListener("message", event => { + if (event.data == "getblob") { + postMessage({ bloburl: testurl }, "*"); + } +}); +// the blob URL should have a content principal +</script> +</body> +</html> diff --git a/docshell/test/browser/file_bug1716290-1.sjs b/docshell/test/browser/file_bug1716290-1.sjs new file mode 100644 index 0000000000..83e6eede3d --- /dev/null +++ b/docshell/test/browser/file_bug1716290-1.sjs @@ -0,0 +1,21 @@ +function handleRequest(request, response) { + if (getState("reloaded") == "reloaded") { + response.setHeader( + "Content-Type", + "text/html; charset=windows-1254", + false + ); + response.write("\u00E4"); + } else { + response.setHeader("Content-Type", "text/html; charset=Shift_JIS", false); + if (getState("loaded") == "loaded") { + setState("reloaded", "reloaded"); + } else { + setState("loaded", "loaded"); + } + // kilobyte to force late-detection reload + response.write("a".repeat(1024)); + response.write("<body>"); + response.write("\u00E4"); + } +} diff --git a/docshell/test/browser/file_bug1716290-2.sjs b/docshell/test/browser/file_bug1716290-2.sjs new file mode 100644 index 0000000000..e695259e30 --- /dev/null +++ b/docshell/test/browser/file_bug1716290-2.sjs @@ -0,0 +1,18 @@ +function handleRequest(request, response) { + if (getState("reloaded") == "reloaded") { + response.setHeader("Content-Type", "text/html", false); + response.write("<meta charset=iso-2022-kr>\u00E4"); + } else { + response.setHeader("Content-Type", "text/html", false); + if (getState("loaded") == "loaded") { + setState("reloaded", "reloaded"); + } else { + setState("loaded", "loaded"); + } + response.write("<meta charset=Shift_JIS>"); + // kilobyte to force late-detection reload + response.write("a".repeat(1024)); + response.write("<body>"); + response.write("\u00E4"); + } +} diff --git a/docshell/test/browser/file_bug1716290-3.sjs b/docshell/test/browser/file_bug1716290-3.sjs new file mode 100644 index 0000000000..7a302e05e4 --- /dev/null +++ b/docshell/test/browser/file_bug1716290-3.sjs @@ -0,0 +1,17 @@ +function handleRequest(request, response) { + if (getState("reloaded") == "reloaded") { + response.setHeader("Content-Type", "text/html; charset=iso-2022-kr", false); + response.write("\u00E4"); + } else { + response.setHeader("Content-Type", "text/html; charset=Shift_JIS", false); + if (getState("loaded") == "loaded") { + setState("reloaded", "reloaded"); + } else { + setState("loaded", "loaded"); + } + // kilobyte to force late-detection reload + response.write("a".repeat(1024)); + response.write("<body>"); + response.write("\u00E4"); + } +} diff --git a/docshell/test/browser/file_bug1716290-4.sjs b/docshell/test/browser/file_bug1716290-4.sjs new file mode 100644 index 0000000000..36753ef532 --- /dev/null +++ b/docshell/test/browser/file_bug1716290-4.sjs @@ -0,0 +1,17 @@ +function handleRequest(request, response) { + if (getState("reloaded") == "reloaded") { + response.setHeader("Content-Type", "text/html", false); + response.write("\u00FE\u00FF\u00E4"); + } else { + response.setHeader("Content-Type", "text/html; charset=Shift_JIS", false); + if (getState("loaded") == "loaded") { + setState("reloaded", "reloaded"); + } else { + setState("loaded", "loaded"); + } + // kilobyte to force late-detection reload + response.write("a".repeat(1024)); + response.write("<body>"); + response.write("\u00E4"); + } +} diff --git a/docshell/test/browser/file_bug1736248-1.html b/docshell/test/browser/file_bug1736248-1.html new file mode 100644 index 0000000000..177acb8f77 --- /dev/null +++ b/docshell/test/browser/file_bug1736248-1.html @@ -0,0 +1,4 @@ +Kilobyte of ASCII followed by UTF-8. +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +Hej världen! diff --git a/docshell/test/browser/file_bug234628-1-child.html b/docshell/test/browser/file_bug234628-1-child.html new file mode 100644 index 0000000000..c36197ac4f --- /dev/null +++ b/docshell/test/browser/file_bug234628-1-child.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>No encoding declaration in parent or child</title> +</head> +<body> +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-1.html b/docshell/test/browser/file_bug234628-1.html new file mode 100644 index 0000000000..11c523ccd9 --- /dev/null +++ b/docshell/test/browser/file_bug234628-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>No encoding declaration in parent or child</title> +</head> +<body> +<h1>No encoding declaration in parent or child</h1> + +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> + +<iframe src="file_bug234628-1-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-10-child.xhtml b/docshell/test/browser/file_bug234628-10-child.xhtml new file mode 100644 index 0000000000..cccf6f2bc0 --- /dev/null +++ b/docshell/test/browser/file_bug234628-10-child.xhtml @@ -0,0 +1,4 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head><title>XML child with no encoding declaration</title></head> +<body><p>Euro sign if decoded as UTF-8: €</p></body> +</html> diff --git a/docshell/test/browser/file_bug234628-10.html b/docshell/test/browser/file_bug234628-10.html new file mode 100644 index 0000000000..78b8f0035d --- /dev/null +++ b/docshell/test/browser/file_bug234628-10.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>No encoding declaration in HTML parent or XHTML child</title> +</head> +<body> +<h1>No encoding declaration in HTML parent or XHTML child</h1> + +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> + +<iframe src="file_bug234628-10-child.xhtml"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-11-child.xhtml b/docshell/test/browser/file_bug234628-11-child.xhtml new file mode 100644 index 0000000000..11ef668b0c --- /dev/null +++ b/docshell/test/browser/file_bug234628-11-child.xhtml @@ -0,0 +1,4 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head><title>No encoding declaration in HTML parent and HTTP declaration in XHTML child</title></head> +<body><p>Euro sign if decoded as UTF-8: €</p></body> +</html> diff --git a/docshell/test/browser/file_bug234628-11-child.xhtml^headers^ b/docshell/test/browser/file_bug234628-11-child.xhtml^headers^ new file mode 100644 index 0000000000..30fb304056 --- /dev/null +++ b/docshell/test/browser/file_bug234628-11-child.xhtml^headers^ @@ -0,0 +1 @@ +Content-Type: application/xhtml+xml; charset=utf-8 diff --git a/docshell/test/browser/file_bug234628-11.html b/docshell/test/browser/file_bug234628-11.html new file mode 100644 index 0000000000..21c5b733e0 --- /dev/null +++ b/docshell/test/browser/file_bug234628-11.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>No encoding declaration in HTML parent and HTTP declaration in XHTML child</title> +</head> +<body> +<h1>No encoding declaration in HTML parent and HTTP declaration in XHTML child</h1> + +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> + +<iframe src="file_bug234628-11-child.xhtml"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-2-child.html b/docshell/test/browser/file_bug234628-2-child.html new file mode 100644 index 0000000000..0acd2e0b27 --- /dev/null +++ b/docshell/test/browser/file_bug234628-2-child.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>No encoding declaration in parent or child</title> +</head> +<body> +<p>Euro sign if decoded as UTF-8: €</p> +<p>a with diaeresis if decoded as UTF-8: ä</p> +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-2.html b/docshell/test/browser/file_bug234628-2.html new file mode 100644 index 0000000000..a87d29e126 --- /dev/null +++ b/docshell/test/browser/file_bug234628-2.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>No encoding declaration in parent or child</title> +</head> +<body> +<h1>No encoding declaration in parent or child</h1> + +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> + +<iframe src="file_bug234628-2-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-3-child.html b/docshell/test/browser/file_bug234628-3-child.html new file mode 100644 index 0000000000..a6ad832310 --- /dev/null +++ b/docshell/test/browser/file_bug234628-3-child.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>meta declaration in parent and child</title> +</head> +<body> +<p>Euro sign if decoded as UTF-8: €</p> +<p>a with diaeresis if decoded as UTF-8: ä</p> +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-3.html b/docshell/test/browser/file_bug234628-3.html new file mode 100644 index 0000000000..8caab60402 --- /dev/null +++ b/docshell/test/browser/file_bug234628-3.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="windows-1252"> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>meta declaration in parent and child</title> +</head> +<body> +<h1>meta declaration in parent and child</h1> + +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> + +<iframe src="file_bug234628-3-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-4-child.html b/docshell/test/browser/file_bug234628-4-child.html new file mode 100644 index 0000000000..f0e7c2c058 --- /dev/null +++ b/docshell/test/browser/file_bug234628-4-child.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>meta declaration in parent and BOM in child</title> +</head> +<body> +<p>Euro sign if decoded as UTF-8: €</p> +<p>a with diaeresis if decoded as UTF-8: ä</p> +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-4.html b/docshell/test/browser/file_bug234628-4.html new file mode 100644 index 0000000000..0137579010 --- /dev/null +++ b/docshell/test/browser/file_bug234628-4.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="windows-1252"> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>meta declaration in parent and BOM in child</title> +</head> +<body> +<h1>meta declaration in parent and BOM in child</h1> + +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> + +<iframe src="file_bug234628-4-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-5-child.html b/docshell/test/browser/file_bug234628-5-child.html Binary files differnew file mode 100644 index 0000000000..a650552f63 --- /dev/null +++ b/docshell/test/browser/file_bug234628-5-child.html diff --git a/docshell/test/browser/file_bug234628-5.html b/docshell/test/browser/file_bug234628-5.html new file mode 100644 index 0000000000..987e6420be --- /dev/null +++ b/docshell/test/browser/file_bug234628-5.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="windows-1252"> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>meta declaration in parent and UTF-16 BOM in child</title> +</head> +<body> +<h1>meta declaration in parent and UTF-16 BOM in child</h1> + +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> + +<iframe src="file_bug234628-5-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-6-child.html b/docshell/test/browser/file_bug234628-6-child.html Binary files differnew file mode 100644 index 0000000000..52c37f2596 --- /dev/null +++ b/docshell/test/browser/file_bug234628-6-child.html diff --git a/docshell/test/browser/file_bug234628-6-child.html^headers^ b/docshell/test/browser/file_bug234628-6-child.html^headers^ new file mode 100644 index 0000000000..bfdcf487fb --- /dev/null +++ b/docshell/test/browser/file_bug234628-6-child.html^headers^ @@ -0,0 +1 @@ +Content-Type: text/html; charset=utf-16be diff --git a/docshell/test/browser/file_bug234628-6.html b/docshell/test/browser/file_bug234628-6.html new file mode 100644 index 0000000000..9d7fc580c3 --- /dev/null +++ b/docshell/test/browser/file_bug234628-6.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="windows-1252"> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>meta declaration in parent and BOMless UTF-16 with HTTP charset in child</title> +</head> +<body> +<h1>meta declaration in parent and BOMless UTF-16 with HTTP charset in child</h1> + +<p>Euro sign if decoded as Windows-1252: </p> +<p>a with diaeresis if decoded as Windows-1252: </p> + +<iframe src="file_bug234628-6-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-8-child.html b/docshell/test/browser/file_bug234628-8-child.html new file mode 100644 index 0000000000..254e0fb2b3 --- /dev/null +++ b/docshell/test/browser/file_bug234628-8-child.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>meta declaration in parent and no declaration in child</title> +</head> +<body> +<p>Capital dje if decoded as Windows-1251: </p> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-8.html b/docshell/test/browser/file_bug234628-8.html new file mode 100644 index 0000000000..b44e91801c --- /dev/null +++ b/docshell/test/browser/file_bug234628-8.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="windows-1251"> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>meta declaration in parent and no declaration in child</title> +</head> +<body> +<h1>meta declaration in parent and no declaration in child</h1> + +<p>Capital dje if decoded as Windows-1251: </p> + +<iframe src="file_bug234628-8-child.html"></iframe> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-9-child.html b/docshell/test/browser/file_bug234628-9-child.html new file mode 100644 index 0000000000..a86b14d7ee --- /dev/null +++ b/docshell/test/browser/file_bug234628-9-child.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>UTF-16 with BOM in parent and no declaration in child</title> +</head> +<body> +<p>Euro sign if decoded as Windows-1251: </p> + +</body> +</html> + diff --git a/docshell/test/browser/file_bug234628-9.html b/docshell/test/browser/file_bug234628-9.html Binary files differnew file mode 100644 index 0000000000..8a469da3aa --- /dev/null +++ b/docshell/test/browser/file_bug234628-9.html diff --git a/docshell/test/browser/file_bug420605.html b/docshell/test/browser/file_bug420605.html new file mode 100644 index 0000000000..8424b92f8f --- /dev/null +++ b/docshell/test/browser/file_bug420605.html @@ -0,0 +1,31 @@ +<head> +<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="/> + <title>Page Title for Bug 420605</title> +</head> +<body> + <h1>Fragment links</h1> + + <p>This page has a bunch of fragment links to sections below:</p> + + <ul> + <li><a id="firefox-link" href="#firefox">Firefox</a></li> + <li><a id="thunderbird-link" href="#thunderbird">Thunderbird</a></li> + <li><a id="seamonkey-link" href="#seamonkey">Seamonkey</a></li> + </ul> + + <p>And here are the sections:</p> + + <h2 id="firefox">Firefox</h2> + + <p>Firefox is a browser.</p> + + <h2 id="thunderbird">Thunderbird</h2> + + <p>Thunderbird is an email client</p> + + <h2 id="seamonkey">Seamonkey</h2> + + <p>Seamonkey is the all-in-one application.</p> + +</body> +</html> diff --git a/docshell/test/browser/file_bug503832.html b/docshell/test/browser/file_bug503832.html new file mode 100644 index 0000000000..338631c8a0 --- /dev/null +++ b/docshell/test/browser/file_bug503832.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> +<!-- +Test page for https://bugzilla.mozilla.org/show_bug.cgi?id=503832 +--> +<head> + <title>Page Title for Bug 503832</title> +</head> +<body> + <h1>Fragment links</h1> + + <p>This page has a bunch of fragment links to sections below:</p> + + <ul> + <li><a id="firefox-link" href="#firefox">Firefox</a></li> + <li><a id="thunderbird-link" href="#thunderbird">Thunderbird</a></li> + <li><a id="seamonkey-link" href="#seamonkey">Seamonkey</a></li> + </ul> + + <p>And here are the sections:</p> + + <h2 id="firefox">Firefox</h2> + + <p>Firefox is a browser.</p> + + <h2 id="thunderbird">Thunderbird</h2> + + <p>Thunderbird is an email client</p> + + <h2 id="seamonkey">Seamonkey</h2> + + <p>Seamonkey is the all-in-one application.</p> + +</body> +</html> diff --git a/docshell/test/browser/file_bug655270.html b/docshell/test/browser/file_bug655270.html new file mode 100644 index 0000000000..0c08d982b1 --- /dev/null +++ b/docshell/test/browser/file_bug655270.html @@ -0,0 +1,11 @@ +<html> + +<head> + <link rel='icon' href='favicon_bug655270.ico'> +</head> + +<body> +Nothing to see here... +</body> + +</html> diff --git a/docshell/test/browser/file_bug670318.html b/docshell/test/browser/file_bug670318.html new file mode 100644 index 0000000000..a78e8fcb19 --- /dev/null +++ b/docshell/test/browser/file_bug670318.html @@ -0,0 +1,23 @@ +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<script> +function load() { + function next() { + if (count < 5) + iframe.src = "data:text/html;charset=utf-8,iframe " + (++count); + } + + var count = 0; + var iframe = document.createElement("iframe"); + iframe.onload = function() { setTimeout(next, 0); }; + document.body.appendChild(iframe); + + setTimeout(next, 0); +} +</script> +</head> + +<body onload="load()"> +Testcase +</body> +</html> diff --git a/docshell/test/browser/file_bug673087-1-child.html b/docshell/test/browser/file_bug673087-1-child.html new file mode 100644 index 0000000000..7bb1ad965b --- /dev/null +++ b/docshell/test/browser/file_bug673087-1-child.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset=windows-1252> +<meta content="width=device-width, initial-scale=1" name="viewport"> +<title>windows-1252 in parent and child, actually EUC-JP</title> +</head> +<body> +<p>Hiragana letter a if decoded as EUC-JP: </p> +<p>ʸ¸Ǥ</p> +</body> +</html> + diff --git a/docshell/test/browser/file_bug673087-1.html b/docshell/test/browser/file_bug673087-1.html Binary files differnew file mode 100644 index 0000000000..3dbea43d66 --- /dev/null +++ b/docshell/test/browser/file_bug673087-1.html diff --git a/docshell/test/browser/file_bug673087-1.html^headers^ b/docshell/test/browser/file_bug673087-1.html^headers^ new file mode 100644 index 0000000000..2340a89c93 --- /dev/null +++ b/docshell/test/browser/file_bug673087-1.html^headers^ @@ -0,0 +1 @@ +Content-Type: text/html; charset=windows-1252 diff --git a/docshell/test/browser/file_bug673087-2.html b/docshell/test/browser/file_bug673087-2.html new file mode 100644 index 0000000000..ccbf896ca6 --- /dev/null +++ b/docshell/test/browser/file_bug673087-2.html @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="ISO-2022-KR"> +FAIL
\ No newline at end of file diff --git a/docshell/test/browser/file_bug852909.pdf b/docshell/test/browser/file_bug852909.pdf Binary files differnew file mode 100644 index 0000000000..89066463f1 --- /dev/null +++ b/docshell/test/browser/file_bug852909.pdf diff --git a/docshell/test/browser/file_bug852909.png b/docshell/test/browser/file_bug852909.png Binary files differnew file mode 100644 index 0000000000..c7510d388f --- /dev/null +++ b/docshell/test/browser/file_bug852909.png diff --git a/docshell/test/browser/file_click_link_within_view_source.html b/docshell/test/browser/file_click_link_within_view_source.html new file mode 100644 index 0000000000..d78e4ba0ff --- /dev/null +++ b/docshell/test/browser/file_click_link_within_view_source.html @@ -0,0 +1,6 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <a id="testlink" href="dummy_page.html">clickme</a> + </body> +</html> diff --git a/docshell/test/browser/file_cross_process_csp_inheritance.html b/docshell/test/browser/file_cross_process_csp_inheritance.html new file mode 100644 index 0000000000..d87761a609 --- /dev/null +++ b/docshell/test/browser/file_cross_process_csp_inheritance.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>Test CSP inheritance if load happens in same and different process</title> + <meta http-equiv="Content-Security-Policy" content="script-src 'none'"> +</head> +<body> + <a href="data:text/html,<html>test-same-diff-process-csp-inhertiance</html>" id="testLink" target="_blank" rel="noopener">click to test same/diff process CSP inheritance</a> +</body> +</html> diff --git a/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html b/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html new file mode 100644 index 0000000000..49341f7481 --- /dev/null +++ b/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test Javascript URI with no script</title> +</head> +<body> +<noscript>no scripts allowed here</noscript> +<a href="javascript:alert(`origin=${origin} location=${location}`)" target="_parent">click me</a> +</body> +</html> diff --git a/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html^headers^ b/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html^headers^ new file mode 100644 index 0000000000..461f7f99ce --- /dev/null +++ b/docshell/test/browser/file_csp_sandbox_no_script_js_uri.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-same-origin allow-top-navigation; diff --git a/docshell/test/browser/file_csp_uir.html b/docshell/test/browser/file_csp_uir.html new file mode 100644 index 0000000000..be60f41a80 --- /dev/null +++ b/docshell/test/browser/file_csp_uir.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1542858 - Test CSP upgrade-insecure-requests</title> + <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> +</head> +<body> + <a id="testlink" href="file_csp_uir_dummy.html">testlink</a> +</body> +</html> diff --git a/docshell/test/browser/file_csp_uir_dummy.html b/docshell/test/browser/file_csp_uir_dummy.html new file mode 100644 index 0000000000..f0ab6775c0 --- /dev/null +++ b/docshell/test/browser/file_csp_uir_dummy.html @@ -0,0 +1 @@ +<html><body>foo</body></html> diff --git a/docshell/test/browser/file_data_load_inherit_csp.html b/docshell/test/browser/file_data_load_inherit_csp.html new file mode 100644 index 0000000000..1efe738e4c --- /dev/null +++ b/docshell/test/browser/file_data_load_inherit_csp.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1358009 - Inherit CSP into data URI</title> + <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline'"> +</head> +<body> + <a id="testlink">testlink</a> +</body> +</html> diff --git a/docshell/test/browser/file_multiple_pushState.html b/docshell/test/browser/file_multiple_pushState.html new file mode 100644 index 0000000000..6592f3f53f --- /dev/null +++ b/docshell/test/browser/file_multiple_pushState.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Test multiple calls to history.pushState</title> + </head> + <body> + <h1>Ohai</h1> + </body> + <script type="text/javascript"> + window.history.pushState({}, "", "/bar/ABC?key=baz"); + let data = new Array(100000).join("a"); + window.history.pushState({ data }, "", "/bar/ABC/DEF?key=baz"); + // Test also Gecko specific state object size limit. + try { + let largeData = new Array(20000000).join("a"); + window.history.pushState({ largeData }, "", "/bar/ABC/DEF/GHI?key=baz"); + } catch (ex) {} + </script> +</html> diff --git a/docshell/test/browser/file_onbeforeunload_0.html b/docshell/test/browser/file_onbeforeunload_0.html new file mode 100644 index 0000000000..7d9acf057d --- /dev/null +++ b/docshell/test/browser/file_onbeforeunload_0.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> +</head> +<body> + <iframe src="http://example.com/browser/docshell/test/browser/file_onbeforeunload_1.html"></iframe> +</body> +</html> diff --git a/docshell/test/browser/file_onbeforeunload_1.html b/docshell/test/browser/file_onbeforeunload_1.html new file mode 100644 index 0000000000..edd27783e4 --- /dev/null +++ b/docshell/test/browser/file_onbeforeunload_1.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> +</head> +<body> + <iframe src="http://mochi.test:8888/browser/docshell/test/browser/file_onbeforeunload_2.html"></iframe> +</body> +</html> diff --git a/docshell/test/browser/file_onbeforeunload_2.html b/docshell/test/browser/file_onbeforeunload_2.html new file mode 100644 index 0000000000..a52a4ace5c --- /dev/null +++ b/docshell/test/browser/file_onbeforeunload_2.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> +</head> +<body> + <iframe src="http://example.com/browser/docshell/test/browser/file_onbeforeunload_3.html"></iframe> +</body> +</html> + diff --git a/docshell/test/browser/file_onbeforeunload_3.html b/docshell/test/browser/file_onbeforeunload_3.html new file mode 100644 index 0000000000..9914f0cd85 --- /dev/null +++ b/docshell/test/browser/file_onbeforeunload_3.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> +</head> +<body> +</body> +</html> + diff --git a/docshell/test/browser/file_open_about_blank.html b/docshell/test/browser/file_open_about_blank.html new file mode 100644 index 0000000000..134384e2f7 --- /dev/null +++ b/docshell/test/browser/file_open_about_blank.html @@ -0,0 +1,2 @@ +<!doctype html> +<button id="open" onclick="window.open('')">Open child window</button> diff --git a/docshell/test/browser/file_slow_load.sjs b/docshell/test/browser/file_slow_load.sjs new file mode 100644 index 0000000000..4c6dd6d5b9 --- /dev/null +++ b/docshell/test/browser/file_slow_load.sjs @@ -0,0 +1,8 @@ +"use strict"; + +function handleRequest(request, response) { + response.processAsync(); + response.setHeader("Content-Type", "text/html"); + response.write("<!doctype html>Loading... "); + // We don't block on this, so it's fine to never finish the response. +} diff --git a/docshell/test/browser/frame-head.js b/docshell/test/browser/frame-head.js new file mode 100644 index 0000000000..4cebb32165 --- /dev/null +++ b/docshell/test/browser/frame-head.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/frame-script */ + +// Functions that are automatically loaded as frame scripts for +// timeline tests. + +// eslint assumes we inherit browser window stuff, but this +// framescript doesn't. +// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +// Functions that look like mochitest functions but forward to the +// browser process. + +this.ok = function (value, message) { + sendAsyncMessage("browser:test:ok", { + value: !!value, + message, + }); +}; + +this.is = function (v1, v2, message) { + ok(v1 == v2, message); +}; + +this.info = function (message) { + sendAsyncMessage("browser:test:info", { message }); +}; + +this.finish = function () { + sendAsyncMessage("browser:test:finish"); +}; + +/* Start a task that runs some timeline tests in the ordinary way. + * + * @param array tests + * The tests to run. This is an array where each element + * is of the form { desc, searchFor, setup, check }. + * + * desc is the test description, a string. + * searchFor is a string or a function + * If a string, then when a marker with this name is + * found, marker-reading is stopped. + * If a function, then the accumulated marker array is + * passed to it, and marker reading stops when it returns + * true. + * setup is a function that takes the docshell as an argument. + * It should start the test. + * check is a function that takes an array of markers + * as an argument and checks the results of the test. + */ +this.timelineContentTest = function (tests) { + (async function () { + let docShell = content.docShell; + + info("Start recording"); + docShell.recordProfileTimelineMarkers = true; + + for (let { desc, searchFor, setup, check } of tests) { + info("Running test: " + desc); + + info("Flushing the previous markers if any"); + docShell.popProfileTimelineMarkers(); + + info("Running the test setup function"); + let onMarkers = timelineWaitForMarkers(docShell, searchFor); + setup(docShell); + info("Waiting for new markers on the docShell"); + let markers = await onMarkers; + + // Cycle collection markers are non-deterministic, and none of these tests + // expect them to show up. + markers = markers.filter(m => !m.name.includes("nsCycleCollector")); + + info("Running the test check function"); + check(markers); + } + + info("Stop recording"); + docShell.recordProfileTimelineMarkers = false; + finish(); + })(); +}; + +function timelineWaitForMarkers(docshell, searchFor) { + if (typeof searchFor == "string") { + let searchForString = searchFor; + let f = function (markers) { + return markers.some(m => m.name == searchForString); + }; + searchFor = f; + } + + return new Promise(function (resolve, reject) { + let waitIterationCount = 0; + let maxWaitIterationCount = 10; // Wait for 2sec maximum + let markers = []; + + setTimeout(function timeoutHandler() { + let newMarkers = docshell.popProfileTimelineMarkers(); + markers = [...markers, ...newMarkers]; + if (searchFor(markers) || waitIterationCount > maxWaitIterationCount) { + resolve(markers); + } else { + setTimeout(timeoutHandler, 200); + waitIterationCount++; + } + }, 200); + }); +} diff --git a/docshell/test/browser/head.js b/docshell/test/browser/head.js new file mode 100644 index 0000000000..e4943bcaf1 --- /dev/null +++ b/docshell/test/browser/head.js @@ -0,0 +1,258 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Helper function for timeline tests. Returns an async task that is + * suitable for use as a particular timeline test. + * @param string frameScriptName + * Base name of the frame script file. + * @param string url + * URL to load. + */ +function makeTimelineTest(frameScriptName, url) { + info("in timelineTest"); + return async function () { + info("in in timelineTest"); + waitForExplicitFinish(); + + await timelineTestOpenUrl(url); + + const here = "chrome://mochitests/content/browser/docshell/test/browser/"; + + let mm = gBrowser.selectedBrowser.messageManager; + mm.loadFrameScript(here + "frame-head.js", false); + mm.loadFrameScript(here + frameScriptName, false); + + // Set up some listeners so that timeline tests running in the + // content process can forward their results to the main process. + mm.addMessageListener("browser:test:ok", function (message) { + ok(message.data.value, message.data.message); + }); + mm.addMessageListener("browser:test:info", function (message) { + info(message.data.message); + }); + mm.addMessageListener("browser:test:finish", function (ignore) { + gBrowser.removeCurrentTab(); + finish(); + }); + }; +} + +/* Open a URL for a timeline test. */ +function timelineTestOpenUrl(url) { + window.focus(); + + let tabSwitchPromise = new Promise((resolve, reject) => { + window.gBrowser.addEventListener( + "TabSwitchDone", + function () { + resolve(); + }, + { once: true } + ); + }); + + let loadPromise = new Promise(function (resolve, reject) { + let browser = window.gBrowser; + let tab = (browser.selectedTab = BrowserTestUtils.addTab(browser, url)); + let linkedBrowser = tab.linkedBrowser; + + BrowserTestUtils.browserLoaded(linkedBrowser).then(() => resolve(tab)); + }); + + return Promise.all([tabSwitchPromise, loadPromise]).then(([_, tab]) => tab); +} + +/** + * Helper function for encoding override tests, loads URL, runs check1, + * forces encoding detection, runs check2. + */ +function runCharsetTest(url, check1, check2) { + waitForExplicitFinish(); + + BrowserTestUtils.openNewForegroundTab(gBrowser, url, true).then(afterOpen); + + function afterOpen() { + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then( + afterChangeCharset + ); + + SpecialPowers.spawn(gBrowser.selectedBrowser, [], check1).then(() => { + BrowserForceEncodingDetection(); + }); + } + + function afterChangeCharset() { + SpecialPowers.spawn(gBrowser.selectedBrowser, [], check2).then(() => { + gBrowser.removeCurrentTab(); + finish(); + }); + } +} + +/** + * Helper function for charset tests. It loads |url| in a new tab, + * runs |check|. + */ +function runCharsetCheck(url, check) { + waitForExplicitFinish(); + + BrowserTestUtils.openNewForegroundTab(gBrowser, url, true).then(afterOpen); + + function afterOpen() { + SpecialPowers.spawn(gBrowser.selectedBrowser, [], check).then(() => { + gBrowser.removeCurrentTab(); + finish(); + }); + } +} + +async function pushState(url, frameId) { + info( + `Doing a pushState, expecting to load ${url} ${ + frameId ? "in an iframe" : "" + }` + ); + let browser = gBrowser.selectedBrowser; + let bc = browser.browsingContext; + if (frameId) { + bc = await SpecialPowers.spawn(bc, [frameId], function (id) { + return content.document.getElementById(id).browsingContext; + }); + } + let loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url); + await SpecialPowers.spawn(bc, [url], function (url) { + content.history.pushState({}, "", url); + }); + await loaded; + info(`Loaded ${url} ${frameId ? "in an iframe" : ""}`); +} + +async function loadURI(url) { + info(`Doing a top-level loadURI, expecting to load ${url}`); + let browser = gBrowser.selectedBrowser; + let loaded = BrowserTestUtils.browserLoaded(browser, false, url); + BrowserTestUtils.loadURIString(browser, url); + await loaded; + info(`Loaded ${url}`); +} + +async function followLink(url, frameId) { + info( + `Creating and following a link to ${url} ${frameId ? "in an iframe" : ""}` + ); + let browser = gBrowser.selectedBrowser; + let bc = browser.browsingContext; + if (frameId) { + bc = await SpecialPowers.spawn(bc, [frameId], function (id) { + return content.document.getElementById(id).browsingContext; + }); + } + let loaded = BrowserTestUtils.browserLoaded(browser, !!frameId, url); + await SpecialPowers.spawn(bc, [url], function (url) { + let a = content.document.createElement("a"); + a.href = url; + content.document.body.appendChild(a); + a.click(); + }); + await loaded; + info(`Loaded ${url} ${frameId ? "in an iframe" : ""}`); +} + +async function goForward(url, useFrame = false) { + info( + `Clicking the forward button, expecting to load ${url} ${ + useFrame ? "in an iframe" : "" + }` + ); + let loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url); + let forwardButton = document.getElementById("forward-button"); + forwardButton.click(); + await loaded; + info(`Loaded ${url} ${useFrame ? "in an iframe" : ""}`); +} + +async function goBack(url, useFrame = false) { + info( + `Clicking the back button, expecting to load ${url} ${ + useFrame ? "in an iframe" : "" + }` + ); + let loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url); + let backButton = document.getElementById("back-button"); + backButton.click(); + await loaded; + info(`Loaded ${url} ${useFrame ? "in an iframe" : ""}`); +} + +function assertBackForwardState(canGoBack, canGoForward) { + let backButton = document.getElementById("back-button"); + let forwardButton = document.getElementById("forward-button"); + + is( + backButton.disabled, + !canGoBack, + `${gBrowser.currentURI.spec}: back button is ${ + canGoBack ? "not" : "" + } disabled` + ); + is( + forwardButton.disabled, + !canGoForward, + `${gBrowser.currentURI.spec}: forward button is ${ + canGoForward ? "not" : "" + } disabled` + ); +} + +class SHListener { + static NewEntry = 0; + static Reload = 1; + static GotoIndex = 2; + static Purge = 3; + static ReplaceEntry = 4; + static async waitForHistory(history, event) { + return new Promise(resolve => { + let listener = { + OnHistoryNewEntry: () => {}, + OnHistoryReload: () => { + return true; + }, + OnHistoryGotoIndex: () => {}, + OnHistoryPurge: () => {}, + OnHistoryReplaceEntry: () => {}, + + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + + function finish() { + history.removeSHistoryListener(listener); + resolve(); + } + switch (event) { + case this.NewEntry: + listener.OnHistoryNewEntry = finish; + break; + case this.Reload: + listener.OnHistoryReload = () => { + finish(); + return true; + }; + break; + case this.GotoIndex: + listener.OnHistoryGotoIndex = finish; + break; + case this.Purge: + listener.OnHistoryPurge = finish; + break; + case this.ReplaceEntry: + listener.OnHistoryReplaceEntry = finish; + break; + } + + history.addSHistoryListener(listener); + }); + } +} diff --git a/docshell/test/browser/head_browser_onbeforeunload.js b/docshell/test/browser/head_browser_onbeforeunload.js new file mode 100644 index 0000000000..6bb334b793 --- /dev/null +++ b/docshell/test/browser/head_browser_onbeforeunload.js @@ -0,0 +1,271 @@ +"use strict"; + +const BASE_URL = "http://mochi.test:8888/browser/docshell/test/browser/"; + +const TEST_PAGE = BASE_URL + "file_onbeforeunload_0.html"; + +const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref( + "prompts.contentPromptSubDialog", + false +); + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +async function withTabModalPromptCount(expected, task) { + const DIALOG_TOPIC = CONTENT_PROMPT_SUBDIALOG + ? "common-dialog-loaded" + : "tabmodal-dialog-loaded"; + + let count = 0; + function observer() { + count++; + } + + Services.obs.addObserver(observer, DIALOG_TOPIC); + try { + return await task(); + } finally { + Services.obs.removeObserver(observer, DIALOG_TOPIC); + is(count, expected, "Should see expected number of tab modal prompts"); + } +} + +function promiseAllowUnloadPrompt(browser, allowNavigation) { + return PromptTestUtils.handleNextPrompt( + browser, + { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" }, + { buttonNumClick: allowNavigation ? 0 : 1 } + ); +} + +// Maintain a pool of background tabs with our test document loaded so +// we don't have to wait for a load prior to each test step (potentially +// tearing down and recreating content processes in the process). +const TabPool = { + poolSize: 5, + + pendingCount: 0, + + readyTabs: [], + + readyPromise: null, + resolveReadyPromise: null, + + spawnTabs() { + while (this.pendingCount + this.readyTabs.length < this.poolSize) { + this.pendingCount++; + let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE); + BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => { + this.readyTabs.push(tab); + this.pendingCount--; + + if (this.resolveReadyPromise) { + this.readyPromise = null; + this.resolveReadyPromise(); + this.resolveReadyPromise = null; + } + + this.spawnTabs(); + }); + } + }, + + getReadyPromise() { + if (!this.readyPromise) { + this.readyPromise = new Promise(resolve => { + this.resolveReadyPromise = resolve; + }); + } + return this.readyPromise; + }, + + async getTab() { + while (!this.readyTabs.length) { + this.spawnTabs(); + await this.getReadyPromise(); + } + + let tab = this.readyTabs.shift(); + this.spawnTabs(); + + gBrowser.selectedTab = tab; + return tab; + }, + + async cleanup() { + this.poolSize = 0; + + while (this.pendingCount) { + await this.getReadyPromise(); + } + + while (this.readyTabs.length) { + await BrowserTestUtils.removeTab(this.readyTabs.shift()); + } + }, +}; + +const ACTIONS = { + NONE: 0, + LISTEN_AND_ALLOW: 1, + LISTEN_AND_BLOCK: 2, +}; + +const ACTION_NAMES = new Map(Object.entries(ACTIONS).map(([k, v]) => [v, k])); + +function* generatePermutations(depth) { + if (depth == 0) { + yield []; + return; + } + for (let subActions of generatePermutations(depth - 1)) { + for (let action of Object.values(ACTIONS)) { + yield [action, ...subActions]; + } + } +} + +const PERMUTATIONS = Array.from(generatePermutations(4)); + +const FRAMES = [ + { process: 0 }, + { process: SpecialPowers.useRemoteSubframes ? 1 : 0 }, + { process: 0 }, + { process: SpecialPowers.useRemoteSubframes ? 1 : 0 }, +]; + +function addListener(bc, block) { + return SpecialPowers.spawn(bc, [block], block => { + return new Promise(resolve => { + function onbeforeunload(event) { + if (block) { + event.preventDefault(); + } + resolve({ event: "beforeunload" }); + } + content.addEventListener("beforeunload", onbeforeunload, { once: true }); + content.unlisten = () => { + content.removeEventListener("beforeunload", onbeforeunload); + }; + + content.addEventListener( + "unload", + () => { + resolve({ event: "unload" }); + }, + { once: true } + ); + }); + }); +} + +function descendants(bc) { + if (bc) { + return [bc, ...descendants(bc.children[0])]; + } + return []; +} + +async function addListeners(frames, actions, startIdx) { + let process = startIdx >= 0 ? FRAMES[startIdx].process : -1; + + let roundTripPromises = []; + + let expectNestedEventLoop = false; + let numBlockers = 0; + let unloadPromises = []; + let beforeUnloadPromises = []; + + for (let [i, frame] of frames.entries()) { + let action = actions[i]; + if (action === ACTIONS.NONE) { + continue; + } + + let block = action === ACTIONS.LISTEN_AND_BLOCK; + let promise = addListener(frame, block); + if (startIdx <= i) { + if (block || FRAMES[i].process !== process) { + expectNestedEventLoop = true; + } + beforeUnloadPromises.push(promise); + numBlockers += block; + } else { + unloadPromises.push(promise); + } + + roundTripPromises.push(SpecialPowers.spawn(frame, [], () => {})); + } + + // Wait for round trip messages to any processes with event listeners to + // return so we're sure that all listeners are registered and their state + // flags are propagated before we continue. + await Promise.all(roundTripPromises); + + return { + expectNestedEventLoop, + expectPrompt: !!numBlockers, + unloadPromises, + beforeUnloadPromises, + }; +} + +async function doTest(actions, startIdx, navigate) { + let tab = await TabPool.getTab(); + let browser = tab.linkedBrowser; + + let frames = descendants(browser.browsingContext); + let expected = await addListeners(frames, actions, startIdx); + + let awaitingPrompt = false; + let promptPromise; + if (expected.expectPrompt) { + awaitingPrompt = true; + promptPromise = promiseAllowUnloadPrompt(browser, false).then(() => { + awaitingPrompt = false; + }); + } + + let promptCount = expected.expectPrompt ? 1 : 0; + await withTabModalPromptCount(promptCount, async () => { + await navigate(tab, frames).then(result => { + ok( + !awaitingPrompt, + "Navigation should not complete while we're still expecting a prompt" + ); + + is( + result.eventLoopSpun, + expected.expectNestedEventLoop, + "Should have nested event loop?" + ); + }); + + for (let result of await Promise.all(expected.beforeUnloadPromises)) { + is( + result.event, + "beforeunload", + "Should have seen beforeunload event before unload" + ); + } + await promptPromise; + + await Promise.all( + frames.map(frame => + SpecialPowers.spawn(frame, [], () => { + if (content.unlisten) { + content.unlisten(); + } + }).catch(() => {}) + ) + ); + + await BrowserTestUtils.removeTab(tab); + }); + + for (let result of await Promise.all(expected.unloadPromises)) { + is(result.event, "unload", "Should have seen unload event"); + } +} diff --git a/docshell/test/browser/onload_message.html b/docshell/test/browser/onload_message.html new file mode 100644 index 0000000000..23f6e37396 --- /dev/null +++ b/docshell/test/browser/onload_message.html @@ -0,0 +1,25 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + addEventListener("load", function() { + // This file is used in couple of different tests. + if (opener) { + opener.postMessage("load", "*"); + } else { + var bc = new BroadcastChannel("browser_browsingContext"); + bc.onmessage = function(event) { + if (event.data == "back") { + bc.close(); + history.back(); + } + }; + bc.postMessage({event: "load", framesLength: frames.length }); + } + }); + </script> + </head> + <body> + This file posts a message containing "load" to opener or BroadcastChannel on load completion. + </body> +</html> diff --git a/docshell/test/browser/onpageshow_message.html b/docshell/test/browser/onpageshow_message.html new file mode 100644 index 0000000000..105c06f8db --- /dev/null +++ b/docshell/test/browser/onpageshow_message.html @@ -0,0 +1,41 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + addEventListener("pageshow", function() { + var bc = new BroadcastChannel("browser_browsingContext"); + function frameData() { + var win = SpecialPowers.wrap(frames[0]); + bc.postMessage( + { framesLength: frames.length, + browsingContextId: win.docShell.browsingContext.id, + outerWindowId: win.docShell.outerWindowID + }); + } + + bc.onmessage = function(event) { + if (event.data == "createiframe") { + let frame = document.createElement("iframe"); + frame.src = "dummy_page.html"; + document.body.appendChild(frame); + frame.onload = frameData; + } else if (event.data == "nextpage") { + bc.close(); + location.href = "onload_message.html"; + } else if (event.data == "queryframes") { + frameData(); + } else if (event.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + } + } + bc.postMessage("pageshow"); + }); + </script> + </head> + <body> + This file posts a message containing "pageshow" to a BroadcastChannel and + keep the channel open for commands. + </body> +</html> diff --git a/docshell/test/browser/overlink_test.html b/docshell/test/browser/overlink_test.html new file mode 100644 index 0000000000..5efd689311 --- /dev/null +++ b/docshell/test/browser/overlink_test.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <head> <meta charset="utf-8"> </head> + <body> + <a id="link" href="https://user:password@example.com">Link</a> + </body> +</html> diff --git a/docshell/test/browser/print_postdata.sjs b/docshell/test/browser/print_postdata.sjs new file mode 100644 index 0000000000..0e3ef38419 --- /dev/null +++ b/docshell/test/browser/print_postdata.sjs @@ -0,0 +1,25 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + if (request.method == "GET") { + response.write(request.queryString); + } else { + var body = new BinaryInputStream(request.bodyInputStream); + + var avail; + var bytes = []; + + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + var data = String.fromCharCode.apply(null, bytes); + response.bodyOutputStream.write(data, data.length); + } +} diff --git a/docshell/test/browser/redirect_to_example.sjs b/docshell/test/browser/redirect_to_example.sjs new file mode 100644 index 0000000000..283e4793db --- /dev/null +++ b/docshell/test/browser/redirect_to_example.sjs @@ -0,0 +1,5 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 302, "Moved Permanently"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + response.setHeader("Location", "http://example"); +} diff --git a/docshell/test/browser/test-form_sjis.html b/docshell/test/browser/test-form_sjis.html new file mode 100644 index 0000000000..91c375deef --- /dev/null +++ b/docshell/test/browser/test-form_sjis.html @@ -0,0 +1,24 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/REC-html401-19991224/strict.dtd"> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=windows-1251"> + <title>Shift_JIS in body and text area</title> + </head> + <body> + <h1>Incorrect meta charset</h1> + <h2>This page is encoded in Shift_JIS, but has an incorrect meta charset + claiming that it is windows-1251</h2> + <p id="testpar">jR[h́AׂĂ̕ɌŗL̔ԍt^܂</p> + <form> + <p> + <textarea id="testtextarea" rows=6 cols=60>jR[h́AׂĂ̕ɌŗL̔ԍt^܂</textarea> + <input id="testinput" type="text" size=60 value="jR[h́AׂĂ̕ɌŗL̔ԍt^܂"> + </p> + </form> + <h2>Expected text on load:</h2> + <p>ѓ†ѓjѓRЃ[ѓh‚НЃA‚·‚Ч‚Д‚М•¶Ћљ‚ЙЊЕ—L‚М”ФЌ†‚р•t—^‚µ‚Ь‚·</p> + <h2>Expected text on resetting the encoding to Shift_JIS:</h2> + <p>ユニコードは、すべての文字に固有の番号を付与します</p> + </body> +</html> diff --git a/docshell/test/chrome/112564_nocache.html b/docshell/test/chrome/112564_nocache.html new file mode 100644 index 0000000000..29fb990b86 --- /dev/null +++ b/docshell/test/chrome/112564_nocache.html @@ -0,0 +1,10 @@ +<html> +<head> +<title>test1</title> +</head> +<body> +<p> +This document will be sent with a no-cache cache-control header. When sent over a secure connection, it should not be stored in bfcache. +</p> +</body> +</html> diff --git a/docshell/test/chrome/112564_nocache.html^headers^ b/docshell/test/chrome/112564_nocache.html^headers^ new file mode 100644 index 0000000000..c829a41ae9 --- /dev/null +++ b/docshell/test/chrome/112564_nocache.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-cache diff --git a/docshell/test/chrome/215405_nocache.html b/docshell/test/chrome/215405_nocache.html new file mode 100644 index 0000000000..c7d48c4eba --- /dev/null +++ b/docshell/test/chrome/215405_nocache.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html style="height: 100%"> +<head> + <title>test1</title> +</head> +<body style="height: 100%"> + <input type="text" id="inp" value=""> + </input> + <div style="height: 50%">Some text</div> + <div style="height: 50%">Some text</div> + <div style="height: 50%">Some text</div> + <div style="height: 50%; width: 300%">Some more text</div> +</body> +</html> diff --git a/docshell/test/chrome/215405_nocache.html^headers^ b/docshell/test/chrome/215405_nocache.html^headers^ new file mode 100644 index 0000000000..c829a41ae9 --- /dev/null +++ b/docshell/test/chrome/215405_nocache.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-cache diff --git a/docshell/test/chrome/215405_nostore.html b/docshell/test/chrome/215405_nostore.html new file mode 100644 index 0000000000..4f5bd0f4f0 --- /dev/null +++ b/docshell/test/chrome/215405_nostore.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html style="height: 100%"> +<head> + <title>test1</title> +</head> +<body style="height: 100%"> + <input type="text" id="inp" value=""> + </input> + <div style="height: 50%">Some text</div> + <div style="height: 50%">Some text</div> + <div style="height: 50%">Some text</div> + <div style="height: 50%; width: 350%">Some more text</div> +</body> +</html> diff --git a/docshell/test/chrome/215405_nostore.html^headers^ b/docshell/test/chrome/215405_nostore.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/chrome/215405_nostore.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/chrome/582176_dummy.html b/docshell/test/chrome/582176_dummy.html new file mode 100644 index 0000000000..3b18e512db --- /dev/null +++ b/docshell/test/chrome/582176_dummy.html @@ -0,0 +1 @@ +hello world diff --git a/docshell/test/chrome/582176_xml.xml b/docshell/test/chrome/582176_xml.xml new file mode 100644 index 0000000000..d3dd576dfe --- /dev/null +++ b/docshell/test/chrome/582176_xml.xml @@ -0,0 +1,2 @@ +<?xml-stylesheet type="text/xsl" href="582176_xslt.xsl"?> +<out/> diff --git a/docshell/test/chrome/582176_xslt.xsl b/docshell/test/chrome/582176_xslt.xsl new file mode 100644 index 0000000000..5957416899 --- /dev/null +++ b/docshell/test/chrome/582176_xslt.xsl @@ -0,0 +1,8 @@ +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:template match="out"> + <html> + <head><title>XSLT result doc</title></head> + <body><p>xslt result</p></body> + </html> + </xsl:template> +</xsl:stylesheet> diff --git a/docshell/test/chrome/662200a.html b/docshell/test/chrome/662200a.html new file mode 100644 index 0000000000..0b9ead6f3e --- /dev/null +++ b/docshell/test/chrome/662200a.html @@ -0,0 +1,8 @@ +<html> + <head> + <title>A</title> + </head> + <body> + <a id="link" href="662200b.html">Next</a> + </body> +</html> diff --git a/docshell/test/chrome/662200b.html b/docshell/test/chrome/662200b.html new file mode 100644 index 0000000000..91e6b971d6 --- /dev/null +++ b/docshell/test/chrome/662200b.html @@ -0,0 +1,8 @@ +<html> + <head> + <title>B</title> + </head> + <body> + <a id="link" href="662200c.html">Next</a> + </body> +</html> diff --git a/docshell/test/chrome/662200c.html b/docshell/test/chrome/662200c.html new file mode 100644 index 0000000000..bc00e6b14b --- /dev/null +++ b/docshell/test/chrome/662200c.html @@ -0,0 +1,7 @@ +<html> + <head> + <title>C</title> + </head> + <body> + </body> +</html> diff --git a/docshell/test/chrome/89419.html b/docshell/test/chrome/89419.html new file mode 100644 index 0000000000..b36b8d788c --- /dev/null +++ b/docshell/test/chrome/89419.html @@ -0,0 +1,7 @@ +<html> +<head> +<title>Bug 89419</title> +</head> +<body> +<img src="http://mochi.test:8888/tests/docshell/test/chrome/bug89419.sjs"> +</body> diff --git a/docshell/test/chrome/92598_nostore.html b/docshell/test/chrome/92598_nostore.html new file mode 100644 index 0000000000..47bb90441e --- /dev/null +++ b/docshell/test/chrome/92598_nostore.html @@ -0,0 +1,10 @@ +<html> +<head> +<title>test1</title> +</head> +<body> +<p> +This document will be sent with a no-store cache-control header. It should not be stored in bfcache. +</p> +</body> +</html> diff --git a/docshell/test/chrome/92598_nostore.html^headers^ b/docshell/test/chrome/92598_nostore.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/chrome/92598_nostore.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/chrome/DocShellHelpers.sys.mjs b/docshell/test/chrome/DocShellHelpers.sys.mjs new file mode 100644 index 0000000000..5f4eee8724 --- /dev/null +++ b/docshell/test/chrome/DocShellHelpers.sys.mjs @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +export class DocShellHelpersParent extends JSWindowActorParent { + static eventListener; + + // These static variables should be set when registering the actor + // (currently doPageNavigation in docshell_helpers.js). + static eventsToListenFor; + static observers; + + constructor() { + super(); + } + receiveMessage({ name, data }) { + if (name == "docshell_helpers:event") { + let { event, originalTargetIsHTMLDocument } = data; + + if (this.constructor.eventsToListenFor.includes(event.type)) { + this.constructor.eventListener(event, originalTargetIsHTMLDocument); + } + } else if (name == "docshell_helpers:observe") { + let { topic } = data; + + this.constructor.observers.get(topic).call(); + } + } +} + +export class DocShellHelpersChild extends JSWindowActorChild { + constructor() { + super(); + } + receiveMessage({ name, data }) { + if (name == "docshell_helpers:preventBFCache") { + // Add an RTCPeerConnection to prevent the page from being bfcached. + let win = this.contentWindow; + win.blockBFCache = new win.RTCPeerConnection(); + } + } + handleEvent(event) { + if ( + Document.isInstance(event.originalTarget) && + event.originalTarget.isInitialDocument + ) { + dump(`TEST: ignoring a ${event.type} event for an initial about:blank\n`); + return; + } + + this.sendAsyncMessage("docshell_helpers:event", { + event: { + type: event.type, + persisted: event.persisted, + originalTarget: { + title: event.originalTarget.title, + location: event.originalTarget.location.href, + visibilityState: event.originalTarget.visibilityState, + hidden: event.originalTarget.hidden, + }, + }, + originalTargetIsHTMLDocument: HTMLDocument.isInstance( + event.originalTarget + ), + }); + } + observe(subject, topic) { + if (Window.isInstance(subject) && subject.document.isInitialDocument) { + dump(`TEST: ignoring a topic notification for an initial about:blank\n`); + return; + } + + this.sendAsyncMessage("docshell_helpers:observe", { topic }); + } +} diff --git a/docshell/test/chrome/allowContentRetargeting.sjs b/docshell/test/chrome/allowContentRetargeting.sjs new file mode 100644 index 0000000000..96e467ef68 --- /dev/null +++ b/docshell/test/chrome/allowContentRetargeting.sjs @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(req, resp) { + resp.setHeader("Content-Type", "application/octet-stream", false); + resp.write("hi"); +} diff --git a/docshell/test/chrome/blue.png b/docshell/test/chrome/blue.png Binary files differnew file mode 100644 index 0000000000..8df58f3a5f --- /dev/null +++ b/docshell/test/chrome/blue.png diff --git a/docshell/test/chrome/bug112564_window.xhtml b/docshell/test/chrome/bug112564_window.xhtml new file mode 100644 index 0000000000..04c25763b3 --- /dev/null +++ b/docshell/test/chrome/bug112564_window.xhtml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="112564Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="112564 test"> + + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + var gTestsIterator; + + function onLoad() { + gTestsIterator = testsIterator(); + nextTest(); + } + + function nextTest() { + gTestsIterator.next(); + } + + function* testsIterator() { + // Load a secure page with a no-cache header, followed by a simple page. + // no-cache should not interfere with the bfcache in the way no-store + // does. + var test1DocURI = "https://example.com:443/chrome/docshell/test/chrome/112564_nocache.html"; + + doPageNavigation({ + uri: test1DocURI, + eventsToListenFor: ["load", "pageshow"], + expectedEvents: [ { type: "load", + title: "test1" }, + { type: "pageshow", + title: "test1", + persisted: false } ], + onNavComplete: nextTest + }); + yield undefined; + + var test2DocURI = "data:text/html,<html><head><title>test2</title></head>" + + "<body>test2</body></html>"; + + doPageNavigation({ + uri: test2DocURI, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "test1", + persisted: true }, + { type: "load", + title: "test2" }, + { type: "pageshow", + title: "test2", + persisted: false } ], + onNavComplete: nextTest + }); + yield undefined; + + // Now go back in history. First page has been cached. + // Check persisted property to confirm + doPageNavigation({ + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "test2", + persisted: true }, + { type: "pageshow", + title: "test1", + persisted: true } ], + onNavComplete: nextTest + }); + yield undefined; + + finish(); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug113934_window.xhtml b/docshell/test/chrome/bug113934_window.xhtml new file mode 100644 index 0000000000..794b8b2836 --- /dev/null +++ b/docshell/test/chrome/bug113934_window.xhtml @@ -0,0 +1,165 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 113934" onload="doTheTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <hbox> + <vbox id="box1"> + </vbox> + <vbox id="box2"> + </vbox> + <spacer flex="1"/> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + /* globals SimpleTest, is, isnot, ok, snapshotWindow, compareSnapshots, + onerror */ + var imports = [ "SimpleTest", "is", "isnot", "ok", "snapshotWindow", + "compareSnapshots", "onerror" ]; + for (var name of imports) { + window[name] = window.arguments[0][name]; + } + + function $(id) { + return document.getElementById(id); + } + + function addBrowser(parent, id, width, height) { + var b = + document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "browser"); + var type = window.location.search.slice(1); + is(type == "chrome" || type == "content", true, "Unexpected type"); + b.setAttribute("type", type); + b.setAttribute("id", id); + b.style.width = width + "px"; + b.style.height = height + "px"; + $(parent).appendChild(b); + } + addBrowser("box1", "f1", 300, 200); + addBrowser("box1", "f2", 300, 200); + addBrowser("box2", "f3", 30, 200); + + /** Test for Bug 113934 */ + var doc1 = + "data:text/html,<html><body onbeforeunload='document.documentElement.textContent = \"\"' onunload='document.documentElement.textContent = \"\"' onpagehide='document.documentElement.textContent = \"\"'>This is a test</body></html>"; + var doc2 = "data:text/html,<html><head></head><body>This is a second test</body></html>"; + + + $("f1").setAttribute("src", doc1); + $("f2").setAttribute("src", doc2); + $("f3").setAttribute("src", doc2); + + async function doTheTest() { + var s2 = await snapshotWindow($("f2").contentWindow); + // var s3 = await snapshotWindow($("f3").contentWindow); + + // This test is broken - see bug 1090274 + //ok(!compareSnapshots(s2, s3, true)[0], + // "Should look different due to different sizing"); + + function getDOM(id) { + return $(id).contentDocument.documentElement.innerHTML; + } + + var dom1 = getDOM("f1"); + + var dom2 = getDOM("f2"); + $("f2").contentDocument.body.textContent = "Modified the text"; + var dom2star = getDOM("f2"); + isnot(dom2, dom2star, "We changed the DOM!"); + + $("f1").swapDocShells($("f2")); + // now we have doms 2*, 1, 2 in the frames + + is(getDOM("f1"), dom2star, "Shouldn't have changed the DOM on swap"); + is(getDOM("f2"), dom1, "Shouldn't have fired event handlers"); + + // Test for bug 480149 + // The DOMLink* events are dispatched asynchronously, thus I cannot + // just include the <link> element in the initial DOM and swap the + // docshells. Instead, the link element is added now. Then, when the + // first DOMLinkAdded event (which is a result of the actual addition) + // is dispatched, the docshells are swapped and the pageshow and pagehide + // events are tested. Only then, we wait for the DOMLink* events, + // which are a result of swapping the docshells. + var DOMLinkListener = { + _afterFirst: false, + _removedDispatched: false, + _addedDispatched: false, + async handleEvent(aEvent) { + if (!this._afterFirst) { + is(aEvent.type, "DOMLinkAdded"); + + var strs = { "f1": "", "f3" : "" }; + function attachListener(node, type) { + var listener = function(e) { + if (strs[node.id]) strs[node.id] += " "; + strs[node.id] += node.id + ".page" + type; + } + node.addEventListener("page" + type, listener); + + listener.detach = function() { + node.removeEventListener("page" + type, listener); + } + return listener; + } + + var l1 = attachListener($("f1"), "show"); + var l2 = attachListener($("f1"), "hide"); + var l3 = attachListener($("f3"), "show"); + var l4 = attachListener($("f3"), "hide"); + + $("f1").swapDocShells($("f3")); + // now we have DOMs 2, 1, 2* in the frames + + l1.detach(); + l2.detach(); + l3.detach(); + l4.detach(); + + // swapDocShells reflows asynchronously, ensure layout is + // clean so that the viewport of f1 is the right size. + $("f1").getBoundingClientRect(); + var s1_new = await snapshotWindow($("f1").contentWindow); + var [same, first, second] = compareSnapshots(s1_new, s2, true); + ok(same, "Should reflow on swap. Expected " + second + " but got " + first); + + is(strs.f1, "f1.pagehide f1.pageshow"); + is(strs.f3, "f3.pagehide f3.pageshow"); + this._afterFirst = true; + return; + } + if (aEvent.type == "DOMLinkAdded") { + is(this._addedDispatched, false); + this._addedDispatched = true; + } + else { + is(this._removedDispatched, false); + this._removedDispatched = true; + } + + if (this._addedDispatched && this._removedDispatched) { + $("f1").removeEventListener("DOMLinkAdded", this); + $("f1").removeEventListener("DOMLinkRemoved", this); + $("f3").removeEventListener("DOMLinkAdded", this); + $("f3").removeEventListener("DOMLinkRemoved", this); + window.close(); + SimpleTest.finish(); + } + } + }; + + $("f1").addEventListener("DOMLinkAdded", DOMLinkListener); + $("f1").addEventListener("DOMLinkRemoved", DOMLinkListener); + $("f3").addEventListener("DOMLinkAdded", DOMLinkListener); + $("f3").addEventListener("DOMLinkRemoved", DOMLinkListener); + + var linkElement = $("f1").contentDocument.createElement("link"); + linkElement.setAttribute("rel", "alternate"); + linkElement.setAttribute("href", "about:blank"); + $("f1").contentDocument.documentElement.firstChild.appendChild(linkElement); + } + + ]]></script> +</window> diff --git a/docshell/test/chrome/bug215405_window.xhtml b/docshell/test/chrome/bug215405_window.xhtml new file mode 100644 index 0000000000..e6dddd99ba --- /dev/null +++ b/docshell/test/chrome/bug215405_window.xhtml @@ -0,0 +1,177 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="215405Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="215405 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false); + + /* globals SimpleTest, is, isnot, ok */ + var imports = [ "SimpleTest", "is", "isnot", "ok"]; + for (var name of imports) { + window[name] = window.arguments[0][name]; + } + + const text="MOZILLA"; + const nostoreURI = "http://mochi.test:8888/tests/docshell/test/chrome/" + + "215405_nostore.html"; + const nocacheURI = "https://example.com:443/tests/docshell/test/chrome/" + + "215405_nocache.html"; + + var gBrowser; + var gTestsIterator; + var currScrollX = 0; + var currScrollY = 0; + + function finish() { + gBrowser.removeEventListener("pageshow", eventListener, true); + // Work around bug 467960 + let historyPurged; + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + let history = gBrowser.browsingContext.sessionHistory; + history.purgeHistory(history.count); + historyPurged = Promise.resolve(); + } else { + historyPurged = SpecialPowers.spawn(gBrowser, [], () => { + let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory + .legacySHistory; + history.purgeHistory(history.count); + }); + } + + historyPurged.then(_ => { + window.close(); + window.arguments[0].SimpleTest.finish(); + }); + } + + function onLoad(e) { + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", eventListener, true); + + gTestsIterator = testsIterator(); + nextTest(); + } + + function eventListener(event) { + setTimeout(nextTest, 0); + } + + function nextTest() { + gTestsIterator.next(); + } + + function* testsIterator() { + // No-store tests + var testName = "[nostore]"; + + // Load a page with a no-store header + BrowserTestUtils.loadURIString(gBrowser, nostoreURI); + yield undefined; + + + // Now that the page has loaded, amend the form contents + var form = gBrowser.contentDocument.getElementById("inp"); + form.value = text; + + // Attempt to scroll the page + var originalXPosition = gBrowser.contentWindow.scrollX; + var originalYPosition = gBrowser.contentWindow.scrollY; + var scrollToX = gBrowser.contentWindow.scrollMaxX; + var scrollToY = gBrowser.contentWindow.scrollMaxY; + gBrowser.contentWindow.scrollBy(scrollToX, scrollToY); + + // Save the scroll position for future comparison + currScrollX = gBrowser.contentWindow.scrollX; + currScrollY = gBrowser.contentWindow.scrollY; + isnot(currScrollX, originalXPosition, + testName + " failed to scroll window horizontally"); + isnot(currScrollY, originalYPosition, + testName + " failed to scroll window vertically"); + + // Load a new document into the browser + var simple = "data:text/html,<html><head><title>test2</title></head>" + + "<body>test2</body></html>"; + BrowserTestUtils.loadURIString(gBrowser, simple); + yield undefined; + + + // Now go back in history. First page should not have been cached. + gBrowser.goBack(); + yield undefined; + + + // First uncacheable page will now be reloaded. Check scroll position + // restored, and form contents not + is(gBrowser.contentWindow.scrollX, currScrollX, testName + + " horizontal axis scroll position not correctly restored"); + is(gBrowser.contentWindow.scrollY, currScrollY, testName + + " vertical axis scroll position not correctly restored"); + var formValue = gBrowser.contentDocument.getElementById("inp").value; + isnot(formValue, text, testName + " form value incorrectly restored"); + + + // https no-cache + testName = "[nocache]"; + + // Load a page with a no-cache header. This should not be + // restricted like no-store (bug 567365) + BrowserTestUtils.loadURIString(gBrowser, nocacheURI); + yield undefined; + + + // Now that the page has loaded, amend the form contents + form = gBrowser.contentDocument.getElementById("inp"); + form.value = text; + + // Attempt to scroll the page + originalXPosition = gBrowser.contentWindow.scrollX; + originalYPosition = gBrowser.contentWindow.scrollY; + scrollToX = gBrowser.contentWindow.scrollMaxX; + scrollToY = gBrowser.contentWindow.scrollMaxY; + gBrowser.contentWindow.scrollBy(scrollToX, scrollToY); + + // Save the scroll position for future comparison + currScrollX = gBrowser.contentWindow.scrollX; + currScrollY = gBrowser.contentWindow.scrollY; + isnot(currScrollX, originalXPosition, + testName + " failed to scroll window horizontally"); + isnot(currScrollY, originalYPosition, + testName + " failed to scroll window vertically"); + + BrowserTestUtils.loadURIString(gBrowser, simple); + yield undefined; + + + // Now go back in history to the cached page. + gBrowser.goBack(); + yield undefined; + + + // First page will now be reloaded. Check scroll position + // and form contents are restored + is(gBrowser.contentWindow.scrollX, currScrollX, testName + + " horizontal axis scroll position not correctly restored"); + is(gBrowser.contentWindow.scrollY, currScrollY, testName + + " vertical axis scroll position not correctly restored"); + formValue = gBrowser.contentDocument.getElementById("inp").value; + is(formValue, text, testName + " form value not correctly restored"); + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); + finish(); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" src="about:blank"/> +</window> diff --git a/docshell/test/chrome/bug293235.html b/docshell/test/chrome/bug293235.html new file mode 100644 index 0000000000..458f88431c --- /dev/null +++ b/docshell/test/chrome/bug293235.html @@ -0,0 +1,13 @@ +<html> + <head> + <title>Bug 293235 page1</title> + <style type="text/css"> + a:visited, a.forcevisited.forcevisited { color: rgb(128, 0, 128); } + a:link, a.forcelink.forcelink { color: rgb(0, 0, 128); } + a:focus { color: rgb(128, 0, 0); } + </style> + </head> + <body> + <a id="link1" href="bug293235_p2.html">This is a test link.</a> + </body> +</html> diff --git a/docshell/test/chrome/bug293235_p2.html b/docshell/test/chrome/bug293235_p2.html new file mode 100644 index 0000000000..2de067b80e --- /dev/null +++ b/docshell/test/chrome/bug293235_p2.html @@ -0,0 +1,8 @@ +<html> + <head> + <title>Bug 293235 page2</title> + </head> + <body> + Nothing to see here, move along. + </body> +</html> diff --git a/docshell/test/chrome/bug293235_window.xhtml b/docshell/test/chrome/bug293235_window.xhtml new file mode 100644 index 0000000000..7d87517824 --- /dev/null +++ b/docshell/test/chrome/bug293235_window.xhtml @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="293235Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(runTests, 0);" + title="bug 293235 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> + + <script type="application/javascript"><![CDATA[ + var {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + + // Return the Element object for the specified element id + function $(id) { return TestWindow.getDocument().getElementById(id); } + + //// + // Generator function for test steps for bug 293235: + // A visited link should have the :visited style applied + // to it when displayed on a page which was fetched from + // the bfcache. + // + async function runTests() { + // Register our observer to know when the link lookup is complete. + let testURI = NetUtil.newURI(getHttpUrl("bug293235_p2.html")); + let os = SpecialPowers.Services.obs; + // Load a test page containing a link that should be initially + // blue, per the :link style. + await new Promise(resolve => { + doPageNavigation({ + uri: getHttpUrl("bug293235.html"), + onNavComplete: resolve, + }); + }); + + // Now that we've been notified, we can check our link color. + // Since we can't use getComputedStyle() for this because + // getComputedStyle lies about styles that result from :visited, + // we have to take snapshots. + // First, take two reference snapshots. + var link1 = $("link1"); + link1.className = "forcelink"; + var refLink = await snapshotWindow(TestWindow.getWindow()); + link1.className = "forcevisited"; + var refVisited = await snapshotWindow(TestWindow.getWindow()); + link1.className = ""; + function snapshotsEqual(snap1, snap2) { + return compareSnapshots(snap1, snap2, true)[0]; + } + ok(!snapshotsEqual(refLink, refVisited), "references should not match"); + ok(snapshotsEqual(refLink, await snapshotWindow(TestWindow.getWindow())), + "link should initially be blue"); + + let observedVisit = false, observedPageShow = false; + await new Promise(resolve => { + function maybeResolve() { + ok(true, "maybe run next test? visited: " + observedVisit + " pageShow: " + observedPageShow); + if (observedVisit && observedPageShow) + resolve(); + } + + // Because adding visits is async, we will not be notified immediately. + let visitObserver = { + observe(aSubject, aTopic, aData) + { + if (!testURI.equals(aSubject.QueryInterface(Ci.nsIURI))) { + return; + } + os.removeObserver(this, aTopic); + observedVisit = true; + maybeResolve(); + }, + }; + os.addObserver(visitObserver, "uri-visit-saved"); + // Load the page that the link on the previous page points to. + doPageNavigation({ + uri: getHttpUrl("bug293235_p2.html"), + onNavComplete() { + observedPageShow = true; + maybeResolve(); + } + }); + }) + + // And the nodes get notified after the "uri-visit-saved" topic, so + // we need to execute soon... + await new Promise(SimpleTest.executeSoon); + + // Go back, verify the original page was loaded from the bfcache, + // and verify that the link is now purple, per the + // :visited style. + await new Promise(resolve => { + doPageNavigation({ + back: true, + eventsToListenFor: ["pageshow"], + expectedEvents: [ { type: "pageshow", + persisted: true, + title: "Bug 293235 page1" } ], + onNavComplete: resolve, + }); + }) + + // Now we can test the link color. + ok(snapshotsEqual(refVisited, await snapshotWindow(TestWindow.getWindow())), + "visited link should be purple"); + + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" src="about:blank"/> +</window> diff --git a/docshell/test/chrome/bug294258_testcase.html b/docshell/test/chrome/bug294258_testcase.html new file mode 100644 index 0000000000..cd80fefd06 --- /dev/null +++ b/docshell/test/chrome/bug294258_testcase.html @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>Bug 294258 Testcase</title> + <meta http-equiv="Content-Type" content="application/xhtml+xml"/> + <style type="text/css"> + * { + font-family: monospace; + } + </style> + </head> + <body> + <div> + <p> + input type="text": <input id="text" type="text"/> + </p> + <p> + input type="checkbox": <input id="checkbox" type="checkbox"/> + </p> + <p> + input type="file": <input id="file" type="file"/> + </p> + <p> + input type="radio": + <input type="radio" id="radio1" name="radio" value="radio1"/> + <input id="radio2" type="radio" name="radio" value="radio2"/> + </p> + <p> + textarea: <textarea id="textarea" rows="4" cols="80"></textarea> + </p> + <p> + select -> option: <select id="select"> + <option>1</option> + <option>2</option> + <option>3</option> + <option>4</option> + </select> + </p> + </div> + </body> +</html> diff --git a/docshell/test/chrome/bug294258_window.xhtml b/docshell/test/chrome/bug294258_window.xhtml new file mode 100644 index 0000000000..fe47f3e70f --- /dev/null +++ b/docshell/test/chrome/bug294258_window.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="294258Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(nextTest, 0);" + title="bug 294258 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + + // Define the generator-iterator for the tests. + var tests = testIterator(); + + //// + // Execute the next test in the generator function. + // + function nextTest() { + tests.next(); + } + + function $(id) { return TestWindow.getDocument().getElementById(id); } + + //// + // Generator function for test steps for bug 294258: + // Form values should be preserved on reload. + // + function* testIterator() + { + // Load a page containing a form. + doPageNavigation( { + uri: getHttpUrl("bug294258_testcase.html"), + onNavComplete: nextTest + } ); + yield undefined; + + // Change the data for each of the form fields, and reload. + $("text").value = "text value"; + $("checkbox").checked = true; + var file = SpecialPowers.Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("294258_test.file"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + let filePath = file.path; + $("file").value = filePath; + $("textarea").value = "textarea value"; + $("radio1").checked = true; + $("select").selectedIndex = 2; + doPageNavigation( { + reload: true, + onNavComplete: nextTest + } ); + yield undefined; + + // Verify that none of the form data has changed. + is($("text").value, "text value", "Text value changed"); + is($("checkbox").checked, true, "Checkbox value changed"); + is($("file").value, filePath, "File value changed"); + is($("textarea").value, "textarea value", "Textarea value changed"); + is($("radio1").checked, true, "Radio value changed"); + is($("select").selectedIndex, 2, "Select value changed"); + + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" src="about:blank"/> +</window> diff --git a/docshell/test/chrome/bug298622_window.xhtml b/docshell/test/chrome/bug298622_window.xhtml new file mode 100644 index 0000000000..38abf35107 --- /dev/null +++ b/docshell/test/chrome/bug298622_window.xhtml @@ -0,0 +1,135 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="298622Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(runTest, 0);" + title="bug 298622 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src= "docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + // Global variable that holds a reference to the find bar. + var gFindBar; + + //// + // Test for bug 298622: + // Find should work correctly on a page loaded from the + // bfcache. + // + async function runTest() + { + // Make sure bfcache is on. + enableBFCache(true); + + // Load a test page which contains some text to be found. + await promisePageNavigation({ + uri: "data:text/html,<html><head><title>test1</title></head>" + + "<body>find this!</body></html>", + }); + + // Load a second, dummy page, verifying that the original + // page gets stored in the bfcache. + await promisePageNavigation({ + uri: getHttpUrl("generic.html"), + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "test1", + persisted: true }, + { type: "pageshow", + title: "generic page" } ], + }); + + // Make sure we unsuppress painting before continuing + await new Promise(resolve => { + SimpleTest.executeSoon(resolve); + }); + + // Search for some text that's on the second page (but not on + // the first page), and verify that it can be found. + gFindBar = document.getElementById("FindToolbar"); + document.getElementById("cmd_find").doCommand(); + ok(!gFindBar.hidden, "failed to open findbar"); + gFindBar._findField.value = "A generic page"; + gFindBar._find(); + await new Promise(resolve => { + SimpleTest.executeSoon(resolve); + }); + + // Make sure Find bar's internal status is not 'notfound' + isnot(gFindBar._findField.getAttribute("status"), "notfound", + "Findfield status attribute should not have been 'notfound'" + + " after Find"); + + // Make sure the key events above have time to be processed + // before continuing + await promiseTrue(() => + SpecialPowers.spawn(document.getElementById("content"), [], function() { + return content.document.getSelection().toString().toLowerCase() == "a generic page"; + }), 20 + ); + + is(gFindBar._findField.value, "A generic page", + "expected text not present in find input field"); + is(await SpecialPowers.spawn(document.getElementById("content"), [], async function() { + return content.document.getSelection().toString().toLowerCase(); + }), + "a generic page", + "find failed on second page loaded"); + + // Go back to the original page and verify it's loaded from the + // bfcache. + await promisePageNavigation({ + back: true, + eventsToListenFor: ["pageshow"], + expectedEvents: [ { type: "pageshow", + title: "test1", + persisted: true } ], + }); + + // Search for some text that's on the original page (but not + // the dummy page loaded above), and verify that it can + // be found. + gFindBar = document.getElementById("FindToolbar"); + document.getElementById("cmd_find").doCommand(); + ok(!gFindBar.hidden, "failed to open findbar"); + gFindBar._findField.value = "find this"; + gFindBar._find(); + await new Promise(resolve => { + SimpleTest.executeSoon(resolve); + }); + + // Make sure Find bar's internal status is not 'notfound' + isnot(gFindBar._findField.getAttribute("status"), "notfound", + "Findfield status attribute should not have been 'notfound'" + + " after Find"); + + // Make sure the key events above have time to be processed + // before continuing + await promiseTrue(() => + SpecialPowers.spawn(document.getElementById("content"), [], function() { + return content.document.getSelection().toString().toLowerCase() == "find this"; + }), 20 + ); + + is(await SpecialPowers.spawn(document.getElementById("content"), [], async function() { + return content.document.getSelection().toString().toLowerCase(); + }), + "find this", + "find failed on page loaded from bfcache"); + + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <commandset> + <command id="cmd_find" + oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" remote="true" maychangeremoteness="true" /> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/docshell/test/chrome/bug301397_1.html b/docshell/test/chrome/bug301397_1.html new file mode 100644 index 0000000000..9943c2efe6 --- /dev/null +++ b/docshell/test/chrome/bug301397_1.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <title>iframe parent</title> + </head> +<body> + <iframe id="iframe" src="bug301397_2.html"/> + </body> +</html> diff --git a/docshell/test/chrome/bug301397_2.html b/docshell/test/chrome/bug301397_2.html new file mode 100644 index 0000000000..4237107060 --- /dev/null +++ b/docshell/test/chrome/bug301397_2.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <title>iframe content #1</title> + </head> +<body> + iframe page 1<br/> + <a id="link" href="bug301397_3.html">go to next page</a> + </body> +</html> diff --git a/docshell/test/chrome/bug301397_3.html b/docshell/test/chrome/bug301397_3.html new file mode 100644 index 0000000000..8d36e92461 --- /dev/null +++ b/docshell/test/chrome/bug301397_3.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <title>iframe content #2</title> + </head> +<body> + iframe page 2<br/> + You made it! + </body> +</html> diff --git a/docshell/test/chrome/bug301397_4.html b/docshell/test/chrome/bug301397_4.html new file mode 100644 index 0000000000..5584a4554a --- /dev/null +++ b/docshell/test/chrome/bug301397_4.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <title>dummy page, no iframe</title> + </head> +<body> + Just a boring test page, nothing special. + </body> +</html> diff --git a/docshell/test/chrome/bug301397_window.xhtml b/docshell/test/chrome/bug301397_window.xhtml new file mode 100644 index 0000000000..cbfa77f7fd --- /dev/null +++ b/docshell/test/chrome/bug301397_window.xhtml @@ -0,0 +1,218 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="301397Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(runTest, 0);" + title="bug 301397 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false); + + //// + // Verifies that the given string exists in the innerHTML of the iframe + // content. + // + async function verifyIframeInnerHtml(string) { + var iframeInnerHtml = await SpecialPowers.spawn(document.getElementById("content"), [string], (string) => + content.document.getElementById("iframe").contentDocument.body.innerHTML); + ok(iframeInnerHtml.includes(string), + "iframe contains wrong document: " + iframeInnerHtml); + } + + //// + // Generator function for test steps for bug 301397: + // The correct page should be displayed in an iframe when + // navigating back and forwards, when the parent page + // occupies multiple spots in the session history. + // + async function runTest() + { + // Make sure the bfcache is enabled. + enableBFCache(8); + + // Load a dummy page. + await promisePageNavigation({ + uri: getHttpUrl("generic.html"), + }); + + // Load a page containing an iframe. + await promisePageNavigation({ + uri: getHttpUrl("bug301397_1.html"), + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "generic page", + persisted: true }, + { type: "pageshow", + title: "iframe content #1", + persisted: false }, // false on initial load + { type: "pageshow", + title: "iframe parent", + persisted: false } ], // false on initial load + }); + + // Click a link in the iframe to cause the iframe to navigate + // to a new page, and wait until the related pagehide/pageshow + // events have occurred. + let waitForLinkNavigation = promisePageEvents({ + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "iframe content #1", + persisted: false }, // false, subframe nav + { type: "pageshow", + title: "iframe content #2", + persisted: false } ], // false on initial load + }); + SpecialPowers.spawn(document.getElementById("content"), [], function() { + let iframe = content.document.getElementById("iframe"); + let link = iframe.contentDocument.getElementById("link"); + let event = iframe.contentDocument.createEvent("MouseEvents"); + event.initMouseEvent("click", true, true, iframe.contentWindow, + 0, 0, 0, 0, 0, + false, false, false, false, + 0, null); + link.dispatchEvent(event); + }); + await waitForLinkNavigation; + + // Load another dummy page. Verify that both the outgoing parent and + // iframe pages are stored in the bfcache. + await promisePageNavigation({ + uri: getHttpUrl("bug301397_4.html"), + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "iframe parent", + persisted: true }, + { type: "pagehide", + title: "iframe content #2", + persisted: true }, + { type: "pageshow", + title: "dummy page, no iframe", + persisted: false } ], // false on initial load + }); + + // Go back. The iframe should show the second page loaded in it. + // Both the parent and the iframe pages should be loaded from + // the bfcache. + await promisePageNavigation({ + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "dummy page, no iframe", + persisted: true }, + { type: "pageshow", + persisted: true, + title: "iframe content #2" }, + { type: "pageshow", + persisted: true, + title: "iframe parent" } ], + }); + + verifyIframeInnerHtml("You made it"); + + // Go gack again. The iframe should show the first page loaded in it. + await promisePageNavigation({ + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "iframe content #2", + persisted: false }, // false, subframe nav + { type: "pageshow", + title: "iframe content #1", + // false since this page was never stored + // in the bfcache in the first place + persisted: false } ], + }); + + verifyIframeInnerHtml("go to next page"); + + // Go back to the generic page. Now go forward to the last page, + // again verifying that the iframe shows the first and second + // pages in order. + await promisePageNavigation({ + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "iframe parent", + persisted: true }, + { type: "pagehide", + title: "iframe content #1", + persisted: true }, + { type: "pageshow", + title: "generic page", + persisted: true } ], + }); + + await promisePageNavigation({ + forward: true, + eventsToListenFor: ["pageshow"], + expectedEvents: [ {type: "pageshow", + title: "iframe content #1", + persisted: true}, + {type: "pageshow", + title: "iframe parent", + persisted: true} ], + }); + + verifyIframeInnerHtml("go to next page"); + + await promisePageNavigation({ + forward: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "iframe content #1", + persisted: false }, // false, subframe nav + { type: "pageshow", + title: "iframe content #2", + // false because the page wasn't stored in + // bfcache last time it was unloaded + persisted: false } ], + }); + + verifyIframeInnerHtml("You made it"); + + await promisePageNavigation({ + forward: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "iframe parent", + persisted: true }, + { type: "pagehide", + title: "iframe content #2", + persisted: true }, + { type: "pageshow", + title: "dummy page, no iframe", + persisted: true } ], + }); + + // Go back once more, and again verify that the iframe shows the + // second page loaded in it. + await promisePageNavigation({ + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "dummy page, no iframe", + persisted: true }, + { type: "pageshow", + persisted: true, + title: "iframe content #2" }, + { type: "pageshow", + persisted: true, + title: "iframe parent" } ], + }); + + verifyIframeInnerHtml("You made it"); + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug303267.html b/docshell/test/chrome/bug303267.html new file mode 100644 index 0000000000..21b9f30311 --- /dev/null +++ b/docshell/test/chrome/bug303267.html @@ -0,0 +1,23 @@ +<html> +<head> + <title> + bug303267.html + </title> + </head> +<body onpageshow="showpageshowcount()"> +<script> +var pageshowcount = 0; +function showpageshowcount() { + pageshowcount++; + var div1 = document.getElementById("div1"); + while (div1.firstChild) { + div1.firstChild.remove(); + } + div1.appendChild(document.createTextNode( + "pageshowcount: " + pageshowcount)); +} +</script> +<div id="div1"> + </div> +</body> +</html> diff --git a/docshell/test/chrome/bug303267_window.xhtml b/docshell/test/chrome/bug303267_window.xhtml new file mode 100644 index 0000000000..741fab0021 --- /dev/null +++ b/docshell/test/chrome/bug303267_window.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="303267Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(runTests, 0);" + title="bug 303267 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + //// + // Bug 303267: When a page is displayed from the bfcache, the script globals should + // remain intact from the page's initial load. + // + async function runTests() + { + // Load an initial test page which should be saved in the bfcache. + var navData = { + uri: getHttpUrl("bug303267.html"), + eventsToListenFor: ["pageshow"], + expectedEvents: [ {type: "pageshow", title: "bug303267.html"} ], + }; + await promisePageNavigation(navData); + + // Save the HTML of the test page for later comparison. + var originalHTML = await getInnerHTMLById("div1"); + + // Load a second test page. The first test page's pagehide event should + // have the .persisted property set to true, indicating that it was + // stored in the bfcache. + navData = { + uri: "data:text/html,<html><head><title>page2</title></head>" + + "<body>bug303267, page2</body></html>", + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ {type: "pagehide", + title: "bug303267.html", + persisted: true}, + {type: "pageshow", + title: "page2"} ], + }; + await promisePageNavigation(navData); + + // Go back. Verify that the pageshow event for the original test page + // had a .persisted property of true, indicating that it came from the + // bfcache. + navData = { + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ {type: "pagehide", + title: "page2"}, + {type: "pageshow", + title: "bug303267.html", + persisted: true} ], + }; + await promisePageNavigation(navData); + + // After going back, if showpagecount() could access a global variable + // and change the test div's innerHTML, then we pass. Otherwise, it + // threw an exception and the following test will fail. + var newHTML = await getInnerHTMLById("div1"); + isnot(originalHTML, + newHTML, "HTML not updated on pageshow; javascript broken?"); + + // Tell the framework the test is finished. + finish(); + } + + //// + // Return the innerHTML of a particular element in the content document. + // + function getInnerHTMLById(id) { + return SpecialPowers.spawn(TestWindow.getBrowser(), [id], (id) => { + return content.document.getElementById(id).innerHTML; + }); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug311007_window.xhtml b/docshell/test/chrome/bug311007_window.xhtml new file mode 100644 index 0000000000..13ae5e16e3 --- /dev/null +++ b/docshell/test/chrome/bug311007_window.xhtml @@ -0,0 +1,204 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="311007Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="startup();" + title="bug 311007 test"> + + <script src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js"></script> + <script type="application/javascript"><![CDATA[ + // `content` is the id of the browser element used for the test. + /* global content */ +/* + Regression test for bug 283733 and bug 307027. + + Bug 283733 + "accessing a relative anchor in a secure page removes the + locked icon and yellow background UI" + + Bug 307027 + "Going back from secure page to error page does not clear yellow bar" + + And enhancements: + + Bug 478927 + onLocationChange should notify whether or not loading an error page. + + */ + +const kDNSErrorURI = "https://example/err.html"; +const kSecureURI = + "https://example.com/tests/docshell/test/navigation/blank.html"; + +/* + Step 1: load a network error page. <err.html> Not Secure + Step 2: load a secure page. <blank.html> Secure + Step 3: a secure page + hashchange. <blank.html#foo> Secure (bug 283733) + Step 4: go back to the error page. <err.html> Not Secure (bug 307027) + */ + +var gListener = null; + +function WebProgressListener() { + this._callback = null; +} + +WebProgressListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + setTimeout(this._callback, 0, aWebProgress, aRequest, aLocation, aFlags); + }, + + set callback(aVal) { + this._callback = aVal; + } +}; + +function startup() { + gListener = new WebProgressListener(); + + document.getElementById("content") + .webProgress + .addProgressListener(gListener, + Ci.nsIWebProgress + .NOTIFY_LOCATION); + + setTimeout(step1A, 0); +} + +/****************************************************************************** + * Step 1: Load an error page, and confirm UA knows it's insecure. + ******************************************************************************/ + +function step1A() { + gListener.callback = step1B; + content.location = kDNSErrorURI; +} + +function step1B(aWebProgress, aRequest, aLocation, aFlags) { + is(aLocation.spec, kDNSErrorURI, "Error page's URI (1)"); + + ok(!(aFlags & Ci.nsIWebProgressListener + .LOCATION_CHANGE_SAME_DOCUMENT), + "DocShell loaded a document (1)"); + + ok((aFlags & Ci.nsIWebProgressListener + .LOCATION_CHANGE_ERROR_PAGE), + "This page is an error page."); + + ok(!(document.getElementById("content") + .browsingContext + .secureBrowserUI.state & + Ci.nsIWebProgressListener.STATE_IS_SECURE), + "This is not a secure page (1)"); + + /* Go to step 2. */ + setTimeout(step2A, 0); +} + +/****************************************************************************** + * Step 2: Load a HTTPS page, and confirm it's secure. + ******************************************************************************/ + +function step2A() { + gListener.callback = step2B; + content.location = kSecureURI; +} + +function step2B(aWebProgress, aRequest, aLocation, aFlags) { + is(aLocation.spec, kSecureURI, "A URI on HTTPS (2)"); + + ok(!(aFlags & Ci.nsIWebProgressListener + .LOCATION_CHANGE_SAME_DOCUMENT), + "DocShell loaded a document (2)"); + + ok(!(aFlags & Ci.nsIWebProgressListener + .LOCATION_CHANGE_ERROR_PAGE), + "This page is not an error page."); + + ok((document.getElementById("content") + .browsingContext + .secureBrowserUI.state & + Ci.nsIWebProgressListener.STATE_IS_SECURE), + "This is a secure page (2)"); + + /* Go to step 3. */ + setTimeout(step3A, 0); +} + +/***************************************************************************** + * Step 3: Trigger hashchange within a secure page, and confirm UA knows + * it's secure. (Bug 283733) + *****************************************************************************/ + +function step3A() { + gListener.callback = step3B; + content.location += "#foo"; +} + +function step3B(aWebProgress, aRequest, aLocation, aFlags) { + is(aLocation.spec, kSecureURI + "#foo", "hashchange on HTTPS (3)"); + + ok((aFlags & Ci.nsIWebProgressListener + .LOCATION_CHANGE_SAME_DOCUMENT), + "We are in the same document as before (3)"); + + ok(!(aFlags & Ci.nsIWebProgressListener + .LOCATION_CHANGE_ERROR_PAGE), + "This page is not an error page."); + + ok((document.getElementById("content") + .browsingContext + .secureBrowserUI.state & + Ci.nsIWebProgressListener.STATE_IS_SECURE), + "This is a secure page (3)"); + + /* Go to step 4. */ + setTimeout(step4A, 0); +} + +/***************************************************************************** + * Step 4: Go back from a secure page to an error page, and confirm UA knows + * it's not secure. (Bug 307027) + *****************************************************************************/ + +function step4A() { + gListener.callback = step4B; + content.history.go(-2); +} + +function step4B(aWebProgress, aRequest, aLocation, aFlags) { + is(aLocation.spec, kDNSErrorURI, "Go back to the error URI (4)"); + + ok(!(aFlags & Ci.nsIWebProgressListener + .LOCATION_CHANGE_SAME_DOCUMENT), + "DocShell loaded a document (4)"); + + ok((aFlags & Ci.nsIWebProgressListener + .LOCATION_CHANGE_ERROR_PAGE), + "This page is an error page."); + + ok(!(document.getElementById("content") + .browsingContext + .secureBrowserUI.state & + Ci.nsIWebProgressListener.STATE_IS_SECURE), + "This is not a secure page (4)"); + + /* End. */ + document.getElementById("content") + .webProgress.removeProgressListener(gListener); + gListener = null; + finish(); +} + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" src="about:blank"/> +</window> diff --git a/docshell/test/chrome/bug321671_window.xhtml b/docshell/test/chrome/bug321671_window.xhtml new file mode 100644 index 0000000000..60ab5e80d0 --- /dev/null +++ b/docshell/test/chrome/bug321671_window.xhtml @@ -0,0 +1,128 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="321671Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(runTest, 0);" + title="bug 321671 test"> + + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false); + + // Maximum number of entries in the bfcache for this session history. + // This number is hardcoded in docshell code. In the test, we'll + // navigate through enough pages so that we hit one that's been + // evicted from the bfcache because it's farther from the current + // page than this number. + const MAX_BFCACHE_PAGES = 3; + + //// + // Bug 321671: Scroll position should be retained when moving backwards and + // forwards through pages when bfcache is enabled. + // + async function runTest() + { + // Variable to hold the scroll positions of the test pages. + var scrollPositions = []; + + // Make sure bfcache is on. + enableBFCache(true); + + // Load enough test pages that so the first one is evicted from the + // bfcache, scroll down on each page, and save the + // current scroll position before continuing. Verify that each + // page we're navigating away from is initially put into the bfcache. + for (var i = 0; i <= MAX_BFCACHE_PAGES + 1; i++) { + let eventsToListenFor = ["pageshow"]; + let expectedEvents = [ { type: "pageshow", + title: "bug321671 page" + (i + 1) } ]; + if (i > 0) { + eventsToListenFor.push("pagehide"); + expectedEvents.unshift({ type: "pagehide", + persisted: true, + title: "bug321671 page" + i }); + } + await promisePageNavigation( { + uri: "data:text/html,<html><head><title>bug321671 page" + (i + 1) + + "</title></head>" + + "<body><table border='1' width='300' height='1000'>" + + "<tbody><tr><td>" + + " page " + (i + 1) + ": foobar foobar foobar foobar " + + "</td></tr></tbody></table> " + + "</body></html>", + eventsToListenFor, + expectedEvents, + } ); + + let { initialScrollY, scrollY } = await SpecialPowers.spawn(TestWindow.getBrowser(), [i], (i) => { + let initialScrollY = content.scrollY; + content.scrollByLines(10 + (2 * i)); + return { initialScrollY, scrollY: content.scrollY }; + }); + is(initialScrollY, 0, + "Page initially has non-zero scrollY position"); + ok(scrollY > 0, + "Page has zero scrollY position after scrolling"); + scrollPositions[i] = scrollY; + } + + // Go back to the first page, one page at a time. For each 'back' + // action, verify that its vertical scroll position is restored + // correctly. Verify that the last page in the sequence + // does not come from the bfcache. Again verify that all pages + // that we navigate away from are initially + // stored in the bfcache. + for (i = MAX_BFCACHE_PAGES + 1; i > 0; i--) { + await promisePageNavigation( { + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "bug321671 page" + (i+1), + persisted: true }, + { type: "pageshow", + title: "bug321671 page" + i, + persisted: i > 1 } ], + } ); + + is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + return content.scrollY; + }), scrollPositions[i-1], + "Scroll position not restored while going back!"); + } + + // Traverse history forward now, and verify scroll position is still + // restored. Similar to the backward traversal, verify that all + // but the last page in the sequence comes from the bfcache. Also + // verify that all of the pages get stored in the bfcache when we + // navigate away from them. + for (i = 1; i <= MAX_BFCACHE_PAGES + 1; i++) { + await promisePageNavigation( { + forward: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + persisted: true, + title: "bug321671 page" + i }, + { type: "pageshow", + persisted: i < MAX_BFCACHE_PAGES + 1, + title: "bug321671 page" + (i + 1) } ], + } ); + + is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + return content.scrollY; + }), scrollPositions[i], + "Scroll position not restored while going forward!"); + } + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug360511_case1.html b/docshell/test/chrome/bug360511_case1.html new file mode 100644 index 0000000000..cca043bb66 --- /dev/null +++ b/docshell/test/chrome/bug360511_case1.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html style="height: 100%"> +<head> + <title> + bug360511 case 1 + </title> + </head> +<body style="height: 100%"> +<a id="link1" href="#bottom">jump to bottom</a> +<div id="div1" style="height: 200%; border: thin solid black;"> + hello large div + </div> + <a name="bottom">here's the bottom of the page</a> +</body> +</html> diff --git a/docshell/test/chrome/bug360511_case2.html b/docshell/test/chrome/bug360511_case2.html new file mode 100644 index 0000000000..217f47724e --- /dev/null +++ b/docshell/test/chrome/bug360511_case2.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html style="height: 100%"> +<head> + <title> + bug360511 case 2 + </title> + </head> +<body style="height: 100%"> +<a id="link1" href="#bottom">jump to bottom</a> +<div id="div1" style="height: 200%; border: thin solid black;"> + hello large div + </div> + <a name="bottom">here's the bottom of the page</a> +</body> +</html> diff --git a/docshell/test/chrome/bug360511_window.xhtml b/docshell/test/chrome/bug360511_window.xhtml new file mode 100644 index 0000000000..cfb0845ef7 --- /dev/null +++ b/docshell/test/chrome/bug360511_window.xhtml @@ -0,0 +1,127 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="360511Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(runTest, 0);" + title="bug 360511 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false); + + function getScrollY() + { + return SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + return content.scrollY; + }); + } + function getLocation() + { + return SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + return content.location.href; + }); + } + + //// + // Bug 360511: Fragment uri's in session history should be restored correctly + // upon back navigation. + // + async function runTest() + { + // Case 1: load a page containing a fragment link; the page should be + // stored in the bfcache. + // Case 2: load a page containing a fragment link; the page should NOT + // be stored in the bfcache. + for (var i = 1; i < 3; i++) + { + var url = "bug360511_case" + i + ".html"; + await promisePageNavigation( { + uri: getHttpUrl(url), + preventBFCache: i != 1 + } ); + + // Store the original url for later comparison. + var originalUrl = TestWindow.getBrowser().currentURI.spec; + var originalDocLocation = await getLocation(); + + // Verify we're at the top of the page. + is(await getScrollY(), 0, "Page initially has a non-zero scrollY property"); + + // Click the on the fragment link in the browser, and use setTimeout + // to give the event a chance to be processed. + await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + var event = content.document.createEvent('MouseEvent'); + event.initMouseEvent("click", true, true, content, 0, + 0, 0, 0, 0, + false, false, false, false, 0, null); + content.document.getElementById("link1").dispatchEvent(event); + }); + await promiseNextPaint(); + + // Verify we're no longer at the top of the page. + await promiseTrue(async function() { + return await getScrollY() > 0; + }, 20); + + // Store the fragment url for later comparison. + var fragmentUrl = TestWindow.getBrowser().currentURI.spec; + let fragDocLocation = await getLocation(); + + // Now navigate to any other page + var expectedPageTitle = "bug360511 case " + i; + await promisePageNavigation( { + uri: getHttpUrl("generic.html"), + eventsToListenFor: ["pagehide", "pageshow"], + expectedEvents: [ {type: "pagehide", title: expectedPageTitle, + persisted: i == 1}, + {type: "pageshow"} ], + } ); + + // Go back + await promisePageNavigation( { + back: true, + eventsToListenFor: ["pageshow"], + expectedEvents: [ {type: "pageshow", title: expectedPageTitle, + persisted: i == 1} ], + } ); + + // Verify the current url is the fragment url + is(TestWindow.getBrowser().currentURI.spec, fragmentUrl, + "current url is not the previous fragment url"); + is(await getLocation(), fragDocLocation, + "document.location is not the previous fragment url"); + + // Go back again. Since we're just going from a fragment url to + // parent url, no pageshow event is fired, so don't wait for any + // events. Rather, just wait for the page's scrollY property to + // change. + var originalScrollY = await getScrollY(); + doPageNavigation( { + back: true, + eventsToListenFor: [] + } ); + await promiseTrue( + async function() { + return (await getScrollY() != originalScrollY); + }, 20); + + // Verify the current url is the original url without fragment + is(TestWindow.getBrowser().currentURI.spec, originalUrl, + "current url is not the original url"); + is(await getLocation(), originalDocLocation, + "document.location is not the original url"); + } + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug364461_window.xhtml b/docshell/test/chrome/bug364461_window.xhtml new file mode 100644 index 0000000000..938a015e73 --- /dev/null +++ b/docshell/test/chrome/bug364461_window.xhtml @@ -0,0 +1,253 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="364461Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="runTest();" + title="364461 test"> + + <script src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false); + + var gBrowser; + + async function runTest() { + gBrowser = document.getElementById("content"); + + // Tests 1 + 2: + // Back/forward between two simple documents. Bfcache will be used. + + var test1Doc = "data:text/html,<html><head><title>test1</title></head>" + + "<body>test1</body></html>"; + + await promisePageNavigation({ + uri: test1Doc, + eventsToListenFor: ["load", "pageshow"], + expectedEvents: [{type: "load", title: "test1"}, + {type: "pageshow", title: "test1", persisted: false}], + }); + + var test2Doc = "data:text/html,<html><head><title>test2</title></head>" + + "<body>test2</body></html>"; + + await promisePageNavigation({ + uri: test2Doc, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test1", persisted: true}, + {type: "load", title: "test2"}, + {type: "pageshow", title: "test2", persisted: false}], + }); + + await promisePageNavigation({ + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test2", persisted: true}, + {type: "pageshow", title: "test1", persisted: true}], + }); + + await promisePageNavigation({ + forward: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test1", persisted: true}, + {type: "pageshow", title: "test2", persisted: true}], + }); + + // Tests 3 + 4: + // Back/forward between a two-level deep iframed document and a simple + // document. Bfcache will be used and events should be dispatched to + // all frames. + + var test3Doc = "data:text/html,<html><head><title>test3</title>" + + "</head><body>" + + "<iframe src='data:text/html," + + "<html><head><title>test3-nested1</title></head>" + + "<body>test3-nested1" + + "<iframe src=\"data:text/html," + + "<html><head><title>test3-nested2</title></head>" + + "<body>test3-nested2</body></html>\">" + + "</iframe>" + + "</body></html>'>" + + "</iframe>" + + "</body></html>"; + + await promisePageNavigation({ + uri: test3Doc, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test2", persisted: true}, + {type: "load", title: "test3-nested2"}, + {type: "pageshow", title: "test3-nested2", persisted: false}, + {type: "load", title: "test3-nested1"}, + {type: "pageshow", title: "test3-nested1", persisted: false}, + {type: "load", title: "test3"}, + {type: "pageshow", title: "test3", persisted: false}], + }); + + var test4Doc = "data:text/html,<html><head><title>test4</title></head>" + + "<body>test4</body></html>"; + + await promisePageNavigation({ + uri: test4Doc, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test3", persisted: true}, + {type: "pagehide", title: "test3-nested1", persisted: true}, + {type: "pagehide", title: "test3-nested2", persisted: true}, + {type: "load", title: "test4"}, + {type: "pageshow", title: "test4", persisted: false}], + }); + + await promisePageNavigation({ + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test4", persisted: true}, + {type: "pageshow", title: "test3-nested2", persisted: true}, + {type: "pageshow", title: "test3-nested1", persisted: true}, + {type: "pageshow", title: "test3", persisted: true}], + }); + + // This is where the two nested pagehide are not dispatched in bug 364461 + await promisePageNavigation({ + forward: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test3", persisted: true}, + {type: "pagehide", title: "test3-nested1", persisted: true}, + {type: "pagehide", title: "test3-nested2", persisted: true}, + {type: "pageshow", title: "test4", persisted: true}], + }); + + // Tests 5 + 6: + // Back/forward between a document containing an unload handler and a + // a simple document. Bfcache won't be used for the first one (see + // http://developer.mozilla.org/en/docs/Using_Firefox_1.5_caching). + + var test5Doc = "data:text/html,<html><head><title>test5</title></head>" + + "<body onunload='while(false) { /* nop */ }'>" + + "test5</body></html>"; + + await promisePageNavigation({ + uri: test5Doc, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test4", persisted: true}, + {type: "load", title: "test5"}, + {type: "pageshow", title: "test5", persisted: false}], + }); + + var test6Doc = "data:text/html,<html><head><title>test6</title></head>" + + "<body>test6</body></html>"; + + await promisePageNavigation({ + uri: test6Doc, + eventsToListenFor: ["load", "unload", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test5", persisted: false}, + {type: "unload", title: "test5"}, + {type: "load", title: "test6"}, + {type: "pageshow", title: "test6", persisted: false}], + }); + + await promisePageNavigation({ + back: true, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test6", persisted: true}, + {type: "load", title: "test5"}, + {type: "pageshow", title: "test5", persisted: false}], + }); + + await promisePageNavigation({ + forward: true, + eventsToListenFor: ["unload", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test5", persisted: false}, + {type: "unload", title: "test5"}, + {type: "pageshow", title: "test6", persisted: true}], + }); + + // Test 7: + // Testcase from https://bugzilla.mozilla.org/show_bug.cgi?id=384977#c10 + // Check that navigation is not blocked after a document is restored + // from bfcache + + var test7Doc = "data:text/html,<html><head><title>test7</title>" + + "</head><body>" + + "<iframe src='data:text/html," + + "<html><head><title>test7-nested1</title></head>" + + "<body>test7-nested1<br/>" + + "<a href=\"data:text/plain,aaa\" target=\"_top\">" + + "Click me, hit back, click me again</a>" + + "</body></html>'>" + + "</iframe>" + + "</body></html>"; + + await promisePageNavigation({ + uri: test7Doc, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test6", persisted: true}, + {type: "load", title: "test7-nested1"}, + {type: "pageshow", title: "test7-nested1", persisted: false}, + {type: "load", title: "test7"}, + {type: "pageshow", title: "test7", persisted: false}], + }); + + // Simulates a click on the link inside the iframe + function clickIframeLink() { + SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + var iframe = content.document.getElementsByTagName("iframe")[0]; + var w = iframe.contentWindow; + var d = iframe.contentDocument; + + var evt = d.createEvent("MouseEvents"); + evt.initMouseEvent("click", true, true, w, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + d.getElementsByTagName("a")[0].dispatchEvent(evt); + }); + } + + let clicked = promisePageNavigation({ + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test7", persisted: true}, + {type: "pagehide", title: "test7-nested1", persisted: true}, + {type: "load"}, + {type: "pageshow", persisted: false}], + waitForEventsOnly: true, + }); + clickIframeLink(); + await clicked; + + is(gBrowser.currentURI.spec, "data:text/plain,aaa", + "Navigation is blocked when clicking link"); + + await promisePageNavigation({ + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", persisted: true}, + {type: "pageshow", title: "test7-nested1", persisted: true}, + {type: "pageshow", title: "test7", persisted: true}], + }); + + clicked = promisePageNavigation({ + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [{type: "pagehide", title: "test7", persisted: true}, + {type: "pagehide", title: "test7-nested1", persisted: true}, + {type: "load"}, + {type: "pageshow", persisted: false}], + waitForEventsOnly: true, + }); + clickIframeLink(); + await clicked; + + is(gBrowser.currentURI.spec, "data:text/plain,aaa", + "Navigation is blocked when clicking link"); + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); + finish(); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug396519_window.xhtml b/docshell/test/chrome/bug396519_window.xhtml new file mode 100644 index 0000000000..a5ecbeb3e8 --- /dev/null +++ b/docshell/test/chrome/bug396519_window.xhtml @@ -0,0 +1,132 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="396519Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="runTest();" + title="396519 test"> + + <script src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false); + + var gTestCount = 0; + + async function navigateAndTest(params, expected) { + await promisePageNavigation(params); + ++gTestCount; + await doTest(expected); + } + + async function doTest(expected) { + function check(testCount, expected) { + let history; + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + history = this.browsingContext.sessionHistory; + } else { + history = this.content.browsingContext.childSessionHistory.legacySHistory; + } + if (history.count == expected.length) { + for (let i = 0; i < history.count; i++) { + var shEntry = history.getEntryAtIndex(i). + QueryInterface(Ci.nsISHEntry); + is(shEntry.isInBFCache, expected[i], `BFCache for shentry[${i}], test ${testCount}`); + } + + // Make sure none of the SHEntries share bfcache entries with one + // another. + for (let i = 0; i < history.count; i++) { + for (let j = 0; j < history.count; j++) { + if (j == i) + continue; + + let shentry1 = history.getEntryAtIndex(i) + .QueryInterface(Ci.nsISHEntry); + let shentry2 = history.getEntryAtIndex(j) + .QueryInterface(Ci.nsISHEntry); + ok(!shentry1.sharesDocumentWith(shentry2), + 'Test ' + testCount + ': shentry[' + i + "] shouldn't " + + "share document with shentry[" + j + ']'); + } + } + } + else { + is(history.count, expected.length, "Wrong history length in test "+testCount); + } + } + + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + check.call(TestWindow.getBrowser(), gTestCount, expected); + } else { + await SpecialPowers.spawn(TestWindow.getBrowser(), [gTestCount, expected], check); + } + } + + async function runTest() { + // Tests 1 + 2: + // Back/forward between two simple documents. Bfcache will be used. + + var test1Doc = "data:text/html,<html><head><title>test1</title></head>" + + "<body>test1</body></html>"; + + await navigateAndTest({ + uri: test1Doc, + }, [false]); + + var test2Doc = test1Doc.replace(/1/,"2"); + + await navigateAndTest({ + uri: test2Doc, + }, [true, false]); + + await navigateAndTest({ + uri: test1Doc, + }, [true, true, false]); + + await navigateAndTest({ + uri: test2Doc, + }, [true, true, true, false]); + + await navigateAndTest({ + uri: test1Doc, + }, [false, true, true, true, false]); + + await navigateAndTest({ + uri: test2Doc, + }, [false, false, true, true, true, false]); + + await navigateAndTest({ + back: true, + }, [false, false, true, true, false, true]); + + await navigateAndTest({ + forward: true, + }, [false, false, true, true, true, false]); + + await navigateAndTest({ + gotoIndex: 1, + }, [false, false, true, true, true, false]); + + await navigateAndTest({ + back: true, + }, [false, true, true, true, false, false]); + + await navigateAndTest({ + gotoIndex: 5, + }, [false, false, true, true, false, false]); + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); + finish(); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug396649_window.xhtml b/docshell/test/chrome/bug396649_window.xhtml new file mode 100644 index 0000000000..c378f11dc3 --- /dev/null +++ b/docshell/test/chrome/bug396649_window.xhtml @@ -0,0 +1,119 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="396649Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(nextTest, 0);" + title="bug 396649 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false); + + // Define the generator-iterator for the tests. + var tests = testIterator(); + + // Maximum number of entries in the bfcache for this session history. + // This number is hardcoded in docshell code. In the test, we'll + // navigate through enough pages so that we hit one that's been + // evicted from the bfcache because it's farther from the current + // page than this number. + const MAX_BFCACHE_PAGES = 3; + + //// + // Execute the next test in the generator function. + // + function nextTest() { + tests.next(); + } + + //// + // Generator function for test steps for bug 396649: Content + // viewers should be evicted from bfcache when going back if more + // than MAX_BFCACHE_PAGES from the current index. + // + function* testIterator() + { + // Make sure bfcache is on. + enableBFCache(true); + + // Load enough pages so that the first loaded is eviced from + // the bfcache, since it is greater the MAX_BFCACHE_PAGES from + // the current position in the session history. Verify all + // of the pages are initially stored in the bfcache when + // they're unloaded. + for (var i = 0; i <= MAX_BFCACHE_PAGES + 1; i++) { + let eventsToListenFor = ["pageshow"]; + let expectedEvents = [ { type: "pageshow", + title: "bug396649 page" + i } ]; + if (i > 0) { + eventsToListenFor.push("pagehide"); + expectedEvents.unshift({ type: "pagehide", + title: "bug396649 page" + (i-1) }); + } + doPageNavigation( { + uri: "data:text/html,<!DOCTYPE html><html>" + + "<head><title>bug396649 page" + i + + "</title></head>" + + "<body>" + + "test page " + i + + "</body></html>", + eventsToListenFor, + expectedEvents, + onNavComplete: nextTest + } ); + yield undefined; + } + + // Go back to the first page, one page at a time. The first + // MAX_BFCACHE_PAGES pages loaded via back should come from the bfcache, + // the last should not, since it should have been evicted during the + // previous navigation sequence. Verify all pages are initially stored + // in the bfcache when they're unloaded. + for (i = MAX_BFCACHE_PAGES + 1; i > 0; i--) { + doPageNavigation( { + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "bug396649 page" + i, + persisted: true }, + { type: "pageshow", + title: "bug396649 page" + (i - 1), + persisted: i > 1 } ], + onNavComplete: nextTest + } ); + yield undefined; + } + + // Traverse history forward now. Again, the first MAX_BFCACHE_PAGES + // pages should come from the bfcache, the last should not, + // since it should have been evicted during the backwards + // traversal above. Verify all pages are initially stored + // in the bfcache when they're unloaded. + for (i = 1; i <= MAX_BFCACHE_PAGES + 1; i++) { + doPageNavigation( { + forward: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "bug396649 page" + (i-1), + persisted: true }, + { type: "pageshow", + title: "bug396649 page" + i, + persisted: i < MAX_BFCACHE_PAGES + 1 } ], + onNavComplete: nextTest + } ); + yield undefined; + } + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug449778_window.xhtml b/docshell/test/chrome/bug449778_window.xhtml new file mode 100644 index 0000000000..3197b7acf4 --- /dev/null +++ b/docshell/test/chrome/bug449778_window.xhtml @@ -0,0 +1,107 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 449778" onload="doTheTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <hbox id="parent"> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + /* globals SimpleTest, is */ + var imports = [ "SimpleTest", "is" ]; + for (var name of imports) { + window[name] = window.arguments[0][name]; + } + + function $(id) { + return document.getElementById(id); + } + + function addBrowser(parent, id, width, height) { + var b = + document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "browser"); + b.setAttribute("type", "content"); + b.setAttribute("id", id); + b.setAttribute("width", width); + b.setAttribute("height", height); + $(parent).appendChild(b); + } + addBrowser("parent", "f1", 300, 200); + addBrowser("parent", "f2", 300, 200); + + /** Test for Bug 449778 */ + var doc1 = "data:text/html,<html><body>This is a test</body></html>"; + var doc2 = "data:text/html,<html><body>This is a second test</body></html>"; + var doc3 = "data:text/html,<html><body>This is a <script>var evt = document.createEvent('Events'); evt.initEvent('testEvt', true, true); document.dispatchEvent(evt);</script>third test</body></html>"; + + + $("f1").setAttribute("src", doc1); + $("f2").setAttribute("src", doc2); + + function doTheTest() { + var strs = { "f1": "", "f2" : "" }; + function attachListener(node, type) { + var listener = function(e) { + if (strs[node.id]) strs[node.id] += " "; + strs[node.id] += node.id + ".page" + type; + } + node.addEventListener("page" + type, listener); + + listener.detach = function() { + node.removeEventListener("page" + type, listener); + } + return listener; + } + + var l1 = attachListener($("f1"), "show"); + var l2 = attachListener($("f1"), "hide"); + var l3 = attachListener($("f2"), "show"); + var l4 = attachListener($("f2"), "hide"); + + $("f1").swapDocShells($("f2")); + + is(strs.f1, "f1.pagehide f1.pageshow", + "Expected hide then show on first loaded page"); + is(strs.f2, "f2.pagehide f2.pageshow", + "Expected hide then show on second loaded page"); + + function listener2() { + $("f2").removeEventListener("testEvt", listener2); + + strs = { "f1": "", "f2" : "" }; + + $("f1").swapDocShells($("f2")); + is(strs.f1, "f1.pagehide", + "Expected hide on already-loaded page, then nothing"); + is(strs.f2, "f2.pageshow f2.pagehide f2.pageshow", + "Expected show on still-loading page, then hide on it, then show " + + "on already-loaded page"); + + strs = { "f1": "", "f2" : "" }; + + $("f1").addEventListener("pageshow", listener3); + } + + function listener3() { + $("f1").removeEventListener("pageshow", listener3); + + is(strs.f1, "f1.pageshow", + "Expected show as our page finishes loading"); + is(strs.f2, "", "Expected no more events here."); + + l1.detach(); + l2.detach(); + l3.detach(); + l4.detach(); + + window.close(); + SimpleTest.finish(); + } + + $("f2").addEventListener("testEvt", listener2, false, true); + $("f2").setAttribute("src", doc3); + } + + ]]></script> +</window> diff --git a/docshell/test/chrome/bug449780_window.xhtml b/docshell/test/chrome/bug449780_window.xhtml new file mode 100644 index 0000000000..c37bc096b2 --- /dev/null +++ b/docshell/test/chrome/bug449780_window.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 449780" onload="setTimeout(doTheTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <hbox id="parent"> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + function addBrowser(parent, width, height) { + var b = + document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "browser"); + b.setAttribute("type", "content"); + b.setAttribute("id", "content"); + b.setAttribute("width", width); + b.setAttribute("height", height); + b.setAttribute("remote", SpecialPowers.Services.appinfo.sessionHistoryInParent); + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + b.setAttribute("maychangeremoteness", "true"); + } + document.getElementById("parent").appendChild(b); + return b; + } + + let f1 = addBrowser("parent", 300, 200); + + /** Test for Bug 449780 */ + var doc1 = "data:text/html,<html><body>This is a test</body></html>"; + var doc2 = "data:text/html,<html><body>This is a second test</body></html>"; + + async function doTheTest() { + await promisePageNavigation({ + uri: doc1, + }); + let { origDOM, modifiedDOM } = await SpecialPowers.spawn(f1, [], () => { + var origDOM = content.document.documentElement.innerHTML; + content.document.body.textContent = "Modified"; + var modifiedDOM = content.document.documentElement.innerHTML; + isnot(origDOM, modifiedDOM, "DOM should be different"); + return { origDOM, modifiedDOM }; + }); + + await promisePageNavigation({ + uri: doc2, + }); + + await promisePageNavigation({ + back: true, + }); + + await SpecialPowers.spawn(f1, [modifiedDOM], (modifiedDOM) => { + is(content.document.documentElement.innerHTML, modifiedDOM, "Should have been bfcached"); + }); + + await promisePageNavigation({ + forward: true, + }); + + f1.removeAttribute("id"); + let f2 = addBrowser("parent", 300, 200); + + // Make sure there's a document or the swap will fail. + await promisePageNavigation({ + uri: "about:blank", + }); + + f1.swapDocShells(f2); + + await promisePageNavigation({ + back: true, + }); + + await SpecialPowers.spawn(f2, [origDOM], (origDOM) => { + is(content.document.documentElement.innerHTML, origDOM, "Should not have been bfcached"); + }); + + finish(); + } + ]]></script> +</window> diff --git a/docshell/test/chrome/bug454235-subframe.xhtml b/docshell/test/chrome/bug454235-subframe.xhtml new file mode 100644 index 0000000000..a8b6178e65 --- /dev/null +++ b/docshell/test/chrome/bug454235-subframe.xhtml @@ -0,0 +1,7 @@ +<window title="Mozilla Bug 454235 SubFrame" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <deck flex="1"> + <browser id="topBrowser" src="about:mozilla"/> + <browser id="burriedBrowser" src="about:mozilla"/> + </deck> +</window> diff --git a/docshell/test/chrome/bug582176_window.xhtml b/docshell/test/chrome/bug582176_window.xhtml new file mode 100644 index 0000000000..b43220a2fe --- /dev/null +++ b/docshell/test/chrome/bug582176_window.xhtml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="303267Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="runTest();" + title="bug 582176 test"> + + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + //// + // Bug 582176. + // + async function runTest() + { + enableBFCache(true); + + var notificationCount = 0; + + let onGlobalCreation = () => { + ++notificationCount; + }; + + await promisePageNavigation({ + uri: "http://mochi.test:8888/chrome/docshell/test/chrome/582176_dummy.html", + onGlobalCreation, + }); + is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + let testVar = content.testVar; + content.testVar = 1; + return testVar; + }), undefined, + "variable unexpectedly there already"); + is(notificationCount, 1, "Should notify on first navigation"); + + await promisePageNavigation({ + uri: "http://mochi.test:8888/chrome/docshell/test/chrome/582176_dummy.html?2", + onGlobalCreation, + }); + is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + return content.testVar; + }), undefined, + "variable should no longer be there"); + is(notificationCount, 2, "Should notify on second navigation"); + + await promisePageNavigation({ + back: true, + }); + is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + return content.testVar; + }), 1, + "variable should still be there"); + is(notificationCount, 2, "Should not notify on back navigation"); + + await promisePageNavigation({ + uri: "http://mochi.test:8888/chrome/docshell/test/chrome/582176_xml.xml", + onGlobalCreation, + }); + is(await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + return content.document.body.textContent; + }), "xslt result", + "Transform performed successfully"); + is(notificationCount, 3, "Should notify only once on XSLT navigation"); + + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug608669.xhtml b/docshell/test/chrome/bug608669.xhtml new file mode 100644 index 0000000000..993f24051c --- /dev/null +++ b/docshell/test/chrome/bug608669.xhtml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Mozilla Bug 608669 - Blank page" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="notifyOpener();"> + <description flex="1" value="This window is intentionally left blank"/> + <script type="application/javascript"> + function notifyOpener() { + if (opener) { + opener.postMessage("load", "*"); + } + } + </script> +</window> diff --git a/docshell/test/chrome/bug662200_window.xhtml b/docshell/test/chrome/bug662200_window.xhtml new file mode 100644 index 0000000000..7c6f656f26 --- /dev/null +++ b/docshell/test/chrome/bug662200_window.xhtml @@ -0,0 +1,119 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="303267Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(runTest, 0);" + title="bug 662200 test"> + + <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false); + + //// + // Bug 662200 + // + async function runTest() + { + // Load the first test page + var navData = { + uri: getHttpUrl("662200a.html"), + eventsToListenFor: ["pageshow"], + expectedEvents: [ {type: "pageshow", title: "A"} ], + }; + await promisePageNavigation(navData); + + // Load the second test page. + navData = { + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ {type: "pagehide", + title: "A"}, + {type: "pageshow", + title: "B"} ], + } + let clicked = promisePageEvents(navData); + SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + var link = content.document.getElementById("link"); + var event = content.document.createEvent("MouseEvents"); + event.initMouseEvent("click", true, true, content, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + link.dispatchEvent(event); + }); + await clicked; + + // Load the third test page. + navData = { + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ {type: "pagehide", + title: "B"}, + {type: "pageshow", + title: "C"} ], + }; + clicked = promisePageEvents(navData); + SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + var link = content.document.getElementById("link"); + var event = content.document.createEvent("MouseEvents"); + event.initMouseEvent("click", true, true, content, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + link.dispatchEvent(event); + }); + await clicked; + + // Go back. + navData = { + back: true, + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ {type: "pagehide", + title: "C"}, + {type: "pageshow", + title: "B"} ], + }; + await promisePageNavigation(navData); + + // Reload. + navData = { + eventsToListenFor: ["pageshow", "pagehide"], + expectedEvents: [ {type: "pagehide", + title: "B"}, + {type: "pageshow", + title: "B"} ], + }; + // Asking the docshell harness to reload for us will call reload on + // nsDocShell which has different behavior than the reload on nsSHistory + // so we call reloadCurrentEntry() (which is equivalent to reload(0) and + // visible from JS) explicitly here. + let reloaded = promisePageEvents(navData); + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + TestWindow.getBrowser().browsingContext.sessionHistory.reloadCurrentEntry(); + } else { + SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory.legacySHistory.reloadCurrentEntry(); + }); + } + await reloaded; + + // After this sequence of events, we should be able to go back and forward + is(TestWindow.getBrowser().canGoBack, true, "Should be able to go back!"); + is(TestWindow.getBrowser().canGoForward, true, "Should be able to go forward!"); + let requestedIndex; + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + requestedIndex = TestWindow.getBrowser().browsingContext.sessionHistory.requestedIndex; + } else { + requestedIndex = await SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + return docShell.sessionHistory.legacySHistory.requestedIndex; + }) + } + is(requestedIndex, -1, "Requested index should be cleared!"); + + Services.prefs.clearUserPref("browser.navigation.requireUserInteraction"); + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" /> +</window> diff --git a/docshell/test/chrome/bug690056_window.xhtml b/docshell/test/chrome/bug690056_window.xhtml new file mode 100644 index 0000000000..fb5dfaea40 --- /dev/null +++ b/docshell/test/chrome/bug690056_window.xhtml @@ -0,0 +1,171 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="690056Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(nextTest, 0);" + title="bug 6500056 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + var tests = testIterator(); + + function nextTest() { + tests.next(); + } + + // Makes sure that we fire the visibilitychange events + function* testIterator() { + // Enable bfcache + enableBFCache(8); + + // Load something for a start + doPageNavigation({ + uri: 'data:text/html,<title>initial load</title>', + onNavComplete: nextTest + }); + yield undefined; + + // Now load a new page + doPageNavigation({ + uri: 'data:text/html,<title>new load</title>', + eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ], + expectedEvents: [ { type: "pagehide", + title: "initial load", + persisted: true }, + { type: "visibilitychange", + title: "initial load", + visibilityState: "hidden", + hidden: true }, + // No visibilitychange events fired for initial pageload + { type: "pageshow", + title: "new load", + persisted: false }, // false on initial load + ], + onNavComplete: nextTest + }); + yield undefined; + + // Now go back + doPageNavigation({ + back: true, + eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ], + expectedEvents: [ { type: "pagehide", + title: "new load", + persisted: true }, + { type: "visibilitychange", + title: "new load", + visibilityState: "hidden", + hidden: true }, + { type: "visibilitychange", + title: "initial load", + visibilityState: "visible", + hidden: false }, + { type: "pageshow", + title: "initial load", + persisted: true }, + ], + onNavComplete: nextTest + }); + yield undefined; + + // And forward + doPageNavigation({ + forward: true, + eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ], + expectedEvents: [ { type: "pagehide", + title: "initial load", + persisted: true }, + { type: "visibilitychange", + title: "initial load", + visibilityState: "hidden", + hidden: true }, + { type: "visibilitychange", + title: "new load", + visibilityState: "visible", + hidden: false }, + { type: "pageshow", + title: "new load", + persisted: true }, + ], + onNavComplete: nextTest + }); + yield undefined; + + waitForPageEvents({ + eventsToListenFor: [ "visibilitychange" ], + expectedEvents: [ { type: "visibilitychange", + title: "new load", + visibilityState: "hidden", + hidden: true }, + ], + onNavComplete: nextTest + }); + + // Now flip our docshell to not active + TestWindow.getBrowser().docShellIsActive = false; + yield undefined; + + // And navigate back; there should be no visibility state transitions + doPageNavigation({ + back: true, + eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ], + expectedEvents: [ { type: "pagehide", + title: "new load", + persisted: true }, + { type: "pageshow", + title: "initial load", + persisted: true }, + ], + unexpectedEvents: [ "visibilitychange" ], + onNavComplete: nextTest + }); + yield undefined; + + waitForPageEvents({ + eventsToListenFor: [ "visibilitychange" ], + expectedEvents: [ { type: "visibilitychange", + title: "initial load", + visibilityState: "visible", + hidden: false }, + ], + onNavComplete: nextTest + }); + + // Now set the docshell active again + TestWindow.getBrowser().docShellIsActive = true; + yield undefined; + + // And forward + doPageNavigation({ + forward: true, + eventsToListenFor: [ "pageshow", "pagehide", "visibilitychange" ], + expectedEvents: [ { type: "pagehide", + title: "initial load", + persisted: true }, + { type: "visibilitychange", + title: "initial load", + visibilityState: "hidden", + hidden: true }, + { type: "visibilitychange", + title: "new load", + visibilityState: "visible", + hidden: false }, + { type: "pageshow", + title: "new load", + persisted: true }, + ], + onNavComplete: nextTest + }); + yield undefined; + + // Tell the framework the test is finished. + finish(); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/bug846906.html b/docshell/test/chrome/bug846906.html new file mode 100644 index 0000000000..a289417ea8 --- /dev/null +++ b/docshell/test/chrome/bug846906.html @@ -0,0 +1,10 @@ +<html> + <head> + <title> + </title> + </head> + <body> + <div id="div1" style="width:1024px; height:768px; border:none;"> + </div> + </body> +</html> diff --git a/docshell/test/chrome/bug89419.sjs b/docshell/test/chrome/bug89419.sjs new file mode 100644 index 0000000000..7172690a9a --- /dev/null +++ b/docshell/test/chrome/bug89419.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + var redirectstate = "/docshell/test/chrome/bug89419.sjs"; + response.setStatusLine("1.1", 302, "Found"); + if (getState(redirectstate) == "") { + response.setHeader("Location", "red.png", false); + setState(redirectstate, "red"); + } else { + response.setHeader("Location", "blue.png", false); + setState(redirectstate, ""); + } + response.setHeader("Cache-Control", "no-cache", false); +} diff --git a/docshell/test/chrome/bug89419_window.xhtml b/docshell/test/chrome/bug89419_window.xhtml new file mode 100644 index 0000000000..12b9dec650 --- /dev/null +++ b/docshell/test/chrome/bug89419_window.xhtml @@ -0,0 +1,69 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="89419Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(runTests, 0);" + title="bug 89419 test"> + + <script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> + + <script type="application/javascript"><![CDATA[ + //// + // A visited link should have the :visited style applied + // to it when displayed on a page which was fetched from + // the bfcache. + // + async function runTests() { + // Disable rcwn to make cache behavior deterministic. + var {SpecialPowers} = window.arguments[0]; + await SpecialPowers.pushPrefEnv({"set":[["network.http.rcwn.enabled", false]]}); + + // Load a test page containing an image referring to the sjs that returns + // a different redirect every time it's loaded. + await new Promise(resolve => { + doPageNavigation({ + uri: getHttpUrl("89419.html"), + onNavComplete: resolve, + preventBFCache: true, + }); + }) + + var first = await snapshotWindow(TestWindow.getWindow()); + + await new Promise(resolve => { + doPageNavigation({ + uri: "about:blank", + onNavComplete: resolve, + }); + }); + + var second = await snapshotWindow(TestWindow.getWindow()); + function snapshotsEqual(snap1, snap2) { + return compareSnapshots(snap1, snap2, true)[0]; + } + ok(!snapshotsEqual(first, second), "about:blank should not be the same as the image web page"); + + await new Promise(resolve => { + doPageNavigation({ + back: true, + onNavComplete: resolve, + }); + }); + + var third = await snapshotWindow(TestWindow.getWindow()); + ok(!snapshotsEqual(third, second), "going back should not be the same as about:blank"); + ok(snapshotsEqual(first, third), "going back should be the same as the initial load"); + + // Tell the framework the test is finished. + finish(); + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" src="about:blank"/> +</window> diff --git a/docshell/test/chrome/bug909218.html b/docshell/test/chrome/bug909218.html new file mode 100644 index 0000000000..a11fa6000d --- /dev/null +++ b/docshell/test/chrome/bug909218.html @@ -0,0 +1,11 @@ +<html>
+<head>
+ <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css">
+ <script src="bug909218.js"></script>
+</head>
+<body>
+ <img src="http://mochi.test:8888/tests/docshell/test/chrome/red.png">
+ <!-- an iframe so we can check these too get the correct flags -->
+ <iframe src="generic.html"/>
+</body>
+</html>
diff --git a/docshell/test/chrome/bug909218.js b/docshell/test/chrome/bug909218.js new file mode 100644 index 0000000000..2222480cd3 --- /dev/null +++ b/docshell/test/chrome/bug909218.js @@ -0,0 +1,2 @@ +// This file exists just to ensure that we load it with the correct flags. +dump("bug909218.js loaded\n"); diff --git a/docshell/test/chrome/bug92598_window.xhtml b/docshell/test/chrome/bug92598_window.xhtml new file mode 100644 index 0000000000..bad91f3df6 --- /dev/null +++ b/docshell/test/chrome/bug92598_window.xhtml @@ -0,0 +1,89 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="92598Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="92598 test"> + + <script src="chrome://mochikit/content/chrome-harness.js" /> + <script type="application/javascript" src="docshell_helpers.js" /> + <script type="application/javascript"><![CDATA[ + var gBrowser; + var gTestsIterator; + + function onLoad() { + gBrowser = document.getElementById("content"); + gTestsIterator = testsIterator(); + nextTest(); + } + + function nextTest() { + gTestsIterator.next(); + } + + function* testsIterator() { + // Load a page with a no-cache header, followed by a simple page + // On pagehide, first page should report it is not being persisted + var test1DocURI = "http://mochi.test:8888/chrome/docshell/test/chrome/92598_nostore.html"; + + doPageNavigation({ + uri: test1DocURI, + eventsToListenFor: ["load", "pageshow"], + expectedEvents: [ { type: "load", + title: "test1" }, + { type: "pageshow", + title: "test1", + persisted: false } ], + onNavComplete: nextTest + }); + yield undefined; + + var test2Doc = "data:text/html,<html><head><title>test2</title></head>" + + "<body>test2</body></html>"; + + doPageNavigation({ + uri: test2Doc, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "test1", + persisted: false }, + { type: "load", + title: "test2" }, + { type: "pageshow", + title: "test2", + persisted: false } ], + onNavComplete: nextTest + }); + yield undefined; + + // Now go back in history. First page should not have been cached. + // Check persisted property to confirm + doPageNavigation({ + back: true, + eventsToListenFor: ["load", "pageshow", "pagehide"], + expectedEvents: [ { type: "pagehide", + title: "test2", + persisted: true }, + { type: "load", + title: "test1" }, + { type: "pageshow", + title: "test1", + persisted: false } ], + onNavComplete: nextTest + }); + yield undefined; + + finish(); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" remote="true" maychangeremoteness="true" /> +</window> diff --git a/docshell/test/chrome/chrome.ini b/docshell/test/chrome/chrome.ini new file mode 100644 index 0000000000..e6294dbb8c --- /dev/null +++ b/docshell/test/chrome/chrome.ini @@ -0,0 +1,107 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + 662200a.html + 662200b.html + 662200c.html + 89419.html + bug113934_window.xhtml + bug215405_window.xhtml + bug293235.html + bug293235_p2.html + bug293235_window.xhtml + bug294258_testcase.html + bug294258_window.xhtml + bug298622_window.xhtml + bug301397_1.html + bug301397_2.html + bug301397_3.html + bug301397_4.html + bug301397_window.xhtml + bug303267.html + bug303267_window.xhtml + bug311007_window.xhtml + bug321671_window.xhtml + bug360511_case1.html + bug360511_case2.html + bug360511_window.xhtml + bug364461_window.xhtml + bug396519_window.xhtml + bug396649_window.xhtml + bug449778_window.xhtml + bug449780_window.xhtml + bug454235-subframe.xhtml + bug608669.xhtml + bug662200_window.xhtml + bug690056_window.xhtml + bug846906.html + bug89419_window.xhtml + bug909218.html + bug909218.js + docshell_helpers.js + DocShellHelpers.sys.mjs + file_viewsource_forbidden_in_iframe.html + generic.html + mozFrameType_window.xhtml + test_docRedirect.sjs + +[test_allowContentRetargeting.html] +[test_bug112564.xhtml] +support-files = + bug112564_window.xhtml + 112564_nocache.html + 112564_nocache.html^headers^ +[test_bug113934.xhtml] +[test_bug215405.xhtml] +[test_bug293235.xhtml] +skip-if = true # bug 1393441 +[test_bug294258.xhtml] +[test_bug298622.xhtml] +[test_bug301397.xhtml] +skip-if = (os == 'win' && processor == 'aarch64') # bug 1533819 +[test_bug303267.xhtml] +[test_bug311007.xhtml] +[test_bug321671.xhtml] +skip-if = + (os == "mac" && !debug) # Bug 1784831 +[test_bug360511.xhtml] +skip-if = (os == 'win' && processor == 'x86') +[test_bug364461.xhtml] +skip-if = (os == 'win' && processor == 'aarch64') # bug 1533814 +[test_bug396519.xhtml] +[test_bug396649.xhtml] +[test_bug428288.html] +[test_bug449778.xhtml] +[test_bug449780.xhtml] +[test_bug453650.xhtml] +[test_bug454235.xhtml] +[test_bug456980.xhtml] +[test_bug565388.xhtml] +skip-if = true # Bug 1026815,Bug 1546159 +[test_bug582176.xhtml] +support-files = + 582176_dummy.html + 582176_xml.xml + 582176_xslt.xsl + bug582176_window.xhtml +[test_bug608669.xhtml] +[test_bug662200.xhtml] +[test_bug690056.xhtml] +[test_bug789773.xhtml] +[test_bug846906.xhtml] +[test_bug89419.xhtml] +[test_bug909218.html] +[test_bug92598.xhtml] +support-files = + 92598_nostore.html + 92598_nostore.html^headers^ + bug92598_window.xhtml +[test_open_and_immediately_close_opener.html] +# This bug only manifests in the non-e10s window open codepath. The test +# should be updated to make sure it still opens a new window in the parent +# process if and when we start running chrome mochitests with e10s enabled. +skip-if = e10s +[test_mozFrameType.xhtml] +[test_viewsource_forbidden_in_iframe.xhtml] +skip-if = true # bug 1019315 +[test_docRedirect.xhtml] diff --git a/docshell/test/chrome/docshell_helpers.js b/docshell/test/chrome/docshell_helpers.js new file mode 100644 index 0000000000..4af1a3f9ea --- /dev/null +++ b/docshell/test/chrome/docshell_helpers.js @@ -0,0 +1,759 @@ +if (!window.opener && window.arguments) { + window.opener = window.arguments[0]; +} +/** + * Import common SimpleTest methods so that they're usable in this window. + */ +/* globals SimpleTest, is, isnot, ok, onerror, todo, todo_is, todo_isnot */ +var imports = [ + "SimpleTest", + "is", + "isnot", + "ok", + "onerror", + "todo", + "todo_is", + "todo_isnot", +]; +for (var name of imports) { + window[name] = window.opener.wrappedJSObject[name]; +} +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +const ACTOR_MODULE_URI = + "chrome://mochitests/content/chrome/docshell/test/chrome/DocShellHelpers.sys.mjs"; +const { DocShellHelpersParent } = ChromeUtils.importESModule(ACTOR_MODULE_URI); +// Some functions assume chrome-harness.js has been loaded. +/* import-globals-from ../../../testing/mochitest/chrome-harness.js */ + +/** + * Define global constants and variables. + */ +const NAV_NONE = 0; +const NAV_BACK = 1; +const NAV_FORWARD = 2; +const NAV_GOTOINDEX = 3; +const NAV_URI = 4; +const NAV_RELOAD = 5; + +var gExpectedEvents; // an array of events which are expected to +// be triggered by this navigation +var gUnexpectedEvents; // an array of event names which are NOT expected +// to be triggered by this navigation +var gFinalEvent; // true if the last expected event has fired +var gUrisNotInBFCache = []; // an array of uri's which shouldn't be stored +// in the bfcache +var gNavType = NAV_NONE; // defines the most recent navigation type +// executed by doPageNavigation +var gOrigMaxTotalViewers = undefined; // original value of max_total_viewers, // to be restored at end of test + +var gExtractedPath = null; // used to cache file path for extracting files from a .jar file + +/** + * The doPageNavigation() function performs page navigations asynchronously, + * listens for specified events, and compares actual events with a list of + * expected events. When all expected events have occurred, an optional + * callback can be notified. The parameter passed to this function is an + * object with the following properties: + * + * uri: if !undefined, the browser will navigate to this uri + * + * back: if true, the browser will execute goBack() + * + * forward: if true, the browser will execute goForward() + * + * gotoIndex: if a number, the browser will execute gotoIndex() with + * the number as index + * + * reload: if true, the browser will execute reload() + * + * eventsToListenFor: an array containing one or more of the following event + * types to listen for: "pageshow", "pagehide", "onload", + * "onunload". If this property is undefined, only a + * single "pageshow" events will be listened for. If this + * property is explicitly empty, [], then no events will + * be listened for. + * + * expectedEvents: an array of one or more expectedEvent objects, + * corresponding to the events which are expected to be + * fired for this navigation. Each object has the + * following properties: + * + * type: one of the event type strings + * title (optional): the title of the window the + * event belongs to + * persisted (optional): the event's expected + * .persisted attribute + * + * This function will verify that events with the + * specified properties are fired in the same order as + * specified in the array. If .title or .persisted + * properties for an expectedEvent are undefined, those + * properties will not be verified for that particular + * event. + * + * This property is ignored if eventsToListenFor is + * undefined or []. + * + * preventBFCache: if true, an RTCPeerConnection will be added to the loaded + * page to prevent it from being bfcached. This property + * has no effect when eventsToListenFor is []. + * + * onNavComplete: a callback which is notified after all expected events + * have occurred, or after a timeout has elapsed. This + * callback is not notified if eventsToListenFor is []. + * onGlobalCreation: a callback which is notified when a DOMWindow is created + * (implemented by observing + * "content-document-global-created") + * + * There must be an expectedEvent object for each event of the types in + * eventsToListenFor which is triggered by this navigation. For example, if + * eventsToListenFor = [ "pagehide", "pageshow" ], then expectedEvents + * must contain an object for each pagehide and pageshow event which occurs as + * a result of this navigation. + */ +// eslint-disable-next-line complexity +function doPageNavigation(params) { + // Parse the parameters. + let back = params.back ? params.back : false; + let forward = params.forward ? params.forward : false; + let gotoIndex = params.gotoIndex ? params.gotoIndex : false; + let reload = params.reload ? params.reload : false; + let uri = params.uri ? params.uri : false; + let eventsToListenFor = + typeof params.eventsToListenFor != "undefined" + ? params.eventsToListenFor + : ["pageshow"]; + gExpectedEvents = + typeof params.eventsToListenFor == "undefined" || !eventsToListenFor.length + ? undefined + : params.expectedEvents; + gUnexpectedEvents = + typeof params.eventsToListenFor == "undefined" || !eventsToListenFor.length + ? undefined + : params.unexpectedEvents; + let preventBFCache = + typeof [params.preventBFCache] == "undefined" + ? false + : params.preventBFCache; + let waitOnly = + typeof params.waitForEventsOnly == "boolean" && params.waitForEventsOnly; + + // Do some sanity checking on arguments. + let navigation = ["back", "forward", "gotoIndex", "reload", "uri"].filter(k => + params.hasOwnProperty(k) + ); + if (navigation.length > 1) { + throw new Error(`Can't specify both ${navigation[0]} and ${navigation[1]}`); + } else if (!navigation.length && !waitOnly) { + throw new Error( + "Must specify back or forward or gotoIndex or reload or uri" + ); + } + if (params.onNavComplete && !eventsToListenFor.length) { + throw new Error("Can't use onNavComplete when eventsToListenFor == []"); + } + if (params.preventBFCache && !eventsToListenFor.length) { + throw new Error("Can't use preventBFCache when eventsToListenFor == []"); + } + if (params.preventBFCache && waitOnly) { + throw new Error("Can't prevent bfcaching when only waiting for events"); + } + if (waitOnly && typeof params.onNavComplete == "undefined") { + throw new Error( + "Must specify onNavComplete when specifying waitForEventsOnly" + ); + } + if (waitOnly && navigation.length) { + throw new Error( + "Can't specify a navigation type when using waitForEventsOnly" + ); + } + for (let anEventType of eventsToListenFor) { + let eventFound = false; + if (anEventType == "pageshow" && !gExpectedEvents) { + eventFound = true; + } + if (gExpectedEvents) { + for (let anExpectedEvent of gExpectedEvents) { + if (anExpectedEvent.type == anEventType) { + eventFound = true; + } + } + } + if (gUnexpectedEvents) { + for (let anExpectedEventType of gUnexpectedEvents) { + if (anExpectedEventType == anEventType) { + eventFound = true; + } + } + } + if (!eventFound) { + throw new Error( + `Event type ${anEventType} is specified in ` + + "eventsToListenFor, but not in expectedEvents" + ); + } + } + + // If the test explicitly sets .eventsToListenFor to [], don't wait for any + // events. + gFinalEvent = !eventsToListenFor.length; + + // Add observers as needed. + let observers = new Map(); + if (params.hasOwnProperty("onGlobalCreation")) { + observers.set("content-document-global-created", params.onGlobalCreation); + } + + // Add an event listener for each type of event in the .eventsToListenFor + // property of the input parameters, and add an observer for all the topics + // in the observers map. + let cleanup; + let useActor = TestWindow.getBrowser().isRemoteBrowser; + if (useActor) { + ChromeUtils.registerWindowActor("DocShellHelpers", { + parent: { + esModuleURI: ACTOR_MODULE_URI, + }, + child: { + esModuleURI: ACTOR_MODULE_URI, + events: { + pageshow: { createActor: true, capture: true }, + pagehide: { createActor: true, capture: true }, + load: { createActor: true, capture: true }, + unload: { createActor: true, capture: true }, + visibilitychange: { createActor: true, capture: true }, + }, + observers: observers.keys(), + }, + allFrames: true, + }); + DocShellHelpersParent.eventsToListenFor = eventsToListenFor; + DocShellHelpersParent.observers = observers; + + cleanup = () => { + DocShellHelpersParent.eventsToListenFor = null; + DocShellHelpersParent.observers = null; + ChromeUtils.unregisterWindowActor("DocShellHelpers"); + }; + } else { + for (let eventType of eventsToListenFor) { + dump("TEST: registering a listener for " + eventType + " events\n"); + TestWindow.getBrowser().addEventListener( + eventType, + pageEventListener, + true + ); + } + if (observers.size > 0) { + let observer = (_, topic) => { + observers.get(topic).call(); + }; + for (let topic of observers.keys()) { + Services.obs.addObserver(observer, topic); + } + + // We only need to do cleanup for the observer, the event listeners will + // go away with the window. + cleanup = () => { + for (let topic of observers.keys()) { + Services.obs.removeObserver(observer, topic); + } + }; + } + } + + if (cleanup) { + // Register a cleanup function on domwindowclosed, to avoid contaminating + // other tests if we bail out early because of an error. + Services.ww.registerNotification(function windowClosed( + subject, + topic, + data + ) { + if (topic == "domwindowclosed" && subject == window) { + Services.ww.unregisterNotification(windowClosed); + cleanup(); + } + }); + } + + // Perform the specified navigation. + if (back) { + gNavType = NAV_BACK; + TestWindow.getBrowser().goBack(); + } else if (forward) { + gNavType = NAV_FORWARD; + TestWindow.getBrowser().goForward(); + } else if (typeof gotoIndex == "number") { + gNavType = NAV_GOTOINDEX; + TestWindow.getBrowser().gotoIndex(gotoIndex); + } else if (uri) { + gNavType = NAV_URI; + BrowserTestUtils.loadURIString(TestWindow.getBrowser(), uri); + } else if (reload) { + gNavType = NAV_RELOAD; + TestWindow.getBrowser().reload(); + } else if (waitOnly) { + gNavType = NAV_NONE; + } else { + throw new Error("No valid navigation type passed to doPageNavigation!"); + } + + // If we're listening for events and there is an .onNavComplete callback, + // wait for all events to occur, and then call doPageNavigation_complete(). + if (eventsToListenFor.length && params.onNavComplete) { + waitForTrue( + function () { + return gFinalEvent; + }, + function () { + doPageNavigation_complete( + eventsToListenFor, + params.onNavComplete, + preventBFCache, + useActor, + cleanup + ); + } + ); + } else if (cleanup) { + cleanup(); + } +} + +/** + * Finish doPageNavigation(), by removing event listeners, adding an unload + * handler if appropriate, and calling the onNavComplete callback. This + * function is called after all the expected events for this navigation have + * occurred. + */ +function doPageNavigation_complete( + eventsToListenFor, + onNavComplete, + preventBFCache, + useActor, + cleanup +) { + if (useActor) { + if (preventBFCache) { + let actor = + TestWindow.getBrowser().browsingContext.currentWindowGlobal.getActor( + "DocShellHelpers" + ); + actor.sendAsyncMessage("docshell_helpers:preventBFCache"); + } + } else { + // Unregister our event listeners. + dump("TEST: removing event listeners\n"); + for (let eventType of eventsToListenFor) { + TestWindow.getBrowser().removeEventListener( + eventType, + pageEventListener, + true + ); + } + + // If the .preventBFCache property was set, add an RTCPeerConnection to + // prevent the page from being bfcached. + if (preventBFCache) { + let win = TestWindow.getWindow(); + win.blockBFCache = new win.RTCPeerConnection(); + } + } + + if (cleanup) { + cleanup(); + } + + let uri = TestWindow.getBrowser().currentURI.spec; + if (preventBFCache) { + // Save the current uri in an array of uri's which shouldn't be + // stored in the bfcache, for later verification. + if (!(uri in gUrisNotInBFCache)) { + gUrisNotInBFCache.push(uri); + } + } else if (gNavType == NAV_URI) { + // If we're navigating to a uri and .preventBFCache was not + // specified, splice it out of gUrisNotInBFCache if it's there. + gUrisNotInBFCache.forEach(function (element, index, array) { + if (element == uri) { + array.splice(index, 1); + } + }, this); + } + + // Notify the callback now that we're done. + onNavComplete.call(); +} + +function promisePageNavigation(params) { + if (params.hasOwnProperty("onNavComplete")) { + throw new Error( + "Can't use a onNavComplete completion callback with promisePageNavigation." + ); + } + return new Promise(resolve => { + params.onNavComplete = resolve; + doPageNavigation(params); + }); +} + +/** + * Allows a test to wait for page navigation events, and notify a + * callback when they've all been received. This works exactly the + * same as doPageNavigation(), except that no navigation is initiated. + */ +function waitForPageEvents(params) { + params.waitForEventsOnly = true; + doPageNavigation(params); +} + +function promisePageEvents(params) { + if (params.hasOwnProperty("onNavComplete")) { + throw new Error( + "Can't use a onNavComplete completion callback with promisePageEvents." + ); + } + return new Promise(resolve => { + params.waitForEventsOnly = true; + params.onNavComplete = resolve; + doPageNavigation(params); + }); +} + +/** + * The event listener which listens for expectedEvents. + */ +function pageEventListener( + event, + originalTargetIsHTMLDocument = HTMLDocument.isInstance(event.originalTarget) +) { + try { + dump( + "TEST: eventListener received a " + + event.type + + " event for page " + + event.originalTarget.title + + ", persisted=" + + event.persisted + + "\n" + ); + } catch (e) { + // Ignore any exception. + } + + // If this page shouldn't be in the bfcache because it was previously + // loaded with .preventBFCache, make sure that its pageshow event + // has .persisted = false, even if the test doesn't explicitly test + // for .persisted. + if ( + event.type == "pageshow" && + (gNavType == NAV_BACK || + gNavType == NAV_FORWARD || + gNavType == NAV_GOTOINDEX) + ) { + let uri = TestWindow.getBrowser().currentURI.spec; + if (uri in gUrisNotInBFCache) { + ok( + !event.persisted, + "pageshow event has .persisted = false, even " + + "though it was loaded with .preventBFCache previously\n" + ); + } + } + + if (typeof gUnexpectedEvents != "undefined") { + is( + gUnexpectedEvents.indexOf(event.type), + -1, + "Should not get unexpected event " + event.type + ); + } + + // If no expected events were specified, mark the final event as having been + // triggered when a pageshow event is fired; this will allow + // doPageNavigation() to return. + if (typeof gExpectedEvents == "undefined" && event.type == "pageshow") { + waitForNextPaint(function () { + gFinalEvent = true; + }); + return; + } + + // If there are explicitly no expected events, but we receive one, it's an + // error. + if (!gExpectedEvents.length) { + ok(false, "Unexpected event (" + event.type + ") occurred"); + return; + } + + // Grab the next expected event, and compare its attributes against the + // actual event. + let expected = gExpectedEvents.shift(); + + is( + event.type, + expected.type, + "A " + + expected.type + + " event was expected, but a " + + event.type + + " event occurred" + ); + + if (typeof expected.title != "undefined") { + ok( + originalTargetIsHTMLDocument, + "originalTarget for last " + event.type + " event not an HTMLDocument" + ); + is( + event.originalTarget.title, + expected.title, + "A " + + event.type + + " event was expected for page " + + expected.title + + ", but was fired for page " + + event.originalTarget.title + ); + } + + if (typeof expected.persisted != "undefined") { + is( + event.persisted, + expected.persisted, + "The persisted property of the " + + event.type + + " event on page " + + event.originalTarget.location + + " had an unexpected value" + ); + } + + if ("visibilityState" in expected) { + is( + event.originalTarget.visibilityState, + expected.visibilityState, + "The visibilityState property of the document on page " + + event.originalTarget.location + + " had an unexpected value" + ); + } + + if ("hidden" in expected) { + is( + event.originalTarget.hidden, + expected.hidden, + "The hidden property of the document on page " + + event.originalTarget.location + + " had an unexpected value" + ); + } + + // If we're out of expected events, let doPageNavigation() return. + if (!gExpectedEvents.length) { + waitForNextPaint(function () { + gFinalEvent = true; + }); + } +} + +DocShellHelpersParent.eventListener = pageEventListener; + +/** + * End a test. + */ +function finish() { + // Work around bug 467960. + let historyPurged; + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + let history = TestWindow.getBrowser().browsingContext?.sessionHistory; + history.purgeHistory(history.count); + historyPurged = Promise.resolve(); + } else { + historyPurged = SpecialPowers.spawn(TestWindow.getBrowser(), [], () => { + let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory + .legacySHistory; + history.purgeHistory(history.count); + }); + } + + // If the test changed the value of max_total_viewers via a call to + // enableBFCache(), then restore it now. + if (typeof gOrigMaxTotalViewers != "undefined") { + Services.prefs.setIntPref( + "browser.sessionhistory.max_total_viewers", + gOrigMaxTotalViewers + ); + } + + // Close the test window and signal the framework that the test is done. + let opener = window.opener; + let SimpleTest = opener.wrappedJSObject.SimpleTest; + + // Wait for the window to be closed before finishing the test + Services.ww.registerNotification(function observer(subject, topic, data) { + if (topic == "domwindowclosed") { + Services.ww.unregisterNotification(observer); + SimpleTest.waitForFocus(SimpleTest.finish, opener); + } + }); + + historyPurged.then(_ => { + window.close(); + }); +} + +/** + * Helper function which waits until another function returns true, or until a + * timeout occurs, and then notifies a callback. + * + * Parameters: + * + * fn: a function which is evaluated repeatedly, and when it turns true, + * the onWaitComplete callback is notified. + * + * onWaitComplete: a callback which will be notified when fn() returns + * true, or when a timeout occurs. + * + * timeout: a timeout, in seconds or ms, after which waitForTrue() will + * fail an assertion and then return, even if the fn function never + * returns true. If timeout is undefined, waitForTrue() will never + * time out. + */ +function waitForTrue(fn, onWaitComplete, timeout) { + promiseTrue(fn, timeout).then(() => { + onWaitComplete.call(); + }); +} + +function promiseTrue(fn, timeout) { + if (typeof timeout != "undefined") { + // If timeoutWait is less than 500, assume it represents seconds, and + // convert to ms. + if (timeout < 500) { + timeout *= 1000; + } + } + + // Loop until the test function returns true, or until a timeout occurs, + // if a timeout is defined. + let intervalid, timeoutid; + let condition = new Promise(resolve => { + intervalid = setInterval(async () => { + if (await fn.call()) { + resolve(); + } + }, 20); + }); + if (typeof timeout != "undefined") { + condition = Promise.race([ + condition, + new Promise((_, reject) => { + timeoutid = setTimeout(() => { + reject(); + }, timeout); + }), + ]); + } + return condition + .finally(() => { + clearInterval(intervalid); + }) + .then(() => { + clearTimeout(timeoutid); + }); +} + +function waitForNextPaint(cb) { + requestAnimationFrame(_ => requestAnimationFrame(cb)); +} + +function promiseNextPaint() { + return new Promise(resolve => { + waitForNextPaint(resolve); + }); +} + +/** + * Enable or disable the bfcache. + * + * Parameters: + * + * enable: if true, set max_total_viewers to -1 (the default); if false, set + * to 0 (disabled), if a number, set it to that specific number + */ +function enableBFCache(enable) { + // If this is the first time the test called enableBFCache(), + // store the original value of max_total_viewers, so it can + // be restored at the end of the test. + if (typeof gOrigMaxTotalViewers == "undefined") { + gOrigMaxTotalViewers = Services.prefs.getIntPref( + "browser.sessionhistory.max_total_viewers" + ); + } + + if (typeof enable == "boolean") { + if (enable) { + Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", -1); + } else { + Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0); + } + } else if (typeof enable == "number") { + Services.prefs.setIntPref( + "browser.sessionhistory.max_total_viewers", + enable + ); + } +} + +/* + * get http root for local tests. Use a single extractJarToTmp instead of + * extracting for each test. + * Returns a file://path if we have a .jar file + */ +function getHttpRoot() { + var location = window.location.href; + location = getRootDirectory(location); + var jar = getJar(location); + if (jar != null) { + if (gExtractedPath == null) { + var resolved = extractJarToTmp(jar); + gExtractedPath = resolved.path; + } + } else { + return null; + } + return "file://" + gExtractedPath + "/"; +} + +/** + * Returns the full HTTP url for a file in the mochitest docshell test + * directory. + */ +function getHttpUrl(filename) { + var root = getHttpRoot(); + if (root == null) { + root = "http://mochi.test:8888/chrome/docshell/test/chrome/"; + } + return root + filename; +} + +/** + * A convenience object with methods that return the current test window, + * browser, and document. + */ +var TestWindow = {}; +TestWindow.getWindow = function () { + return document.getElementById("content").contentWindow; +}; +TestWindow.getBrowser = function () { + return document.getElementById("content"); +}; +TestWindow.getDocument = function () { + return document.getElementById("content").contentDocument; +}; diff --git a/docshell/test/chrome/file_viewsource_forbidden_in_iframe.html b/docshell/test/chrome/file_viewsource_forbidden_in_iframe.html new file mode 100644 index 0000000000..fdecbbdfe1 --- /dev/null +++ b/docshell/test/chrome/file_viewsource_forbidden_in_iframe.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test ifranes for view-source forbidden in iframe tests</title> +</head> +<body> + <iframe id="testIframe"></iframe> + <iframe id="refIframe"></iframe> +</body> +</html> diff --git a/docshell/test/chrome/gen_template.pl b/docshell/test/chrome/gen_template.pl new file mode 100644 index 0000000000..109d6161cd --- /dev/null +++ b/docshell/test/chrome/gen_template.pl @@ -0,0 +1,39 @@ +#!/usr/bin/perl + +# This script makes docshell test case templates. It takes one argument: +# +# -b: a bugnumber +# +# For example, this command: +# +# perl gen_template.pl -b 303267 +# +# Writes test case template files test_bug303267.xhtml and bug303267_window.xhtml +# to the current directory. + +use FindBin; +use Getopt::Long; +GetOptions("b=i"=> \$bug_number); + +$template = "$FindBin::RealBin/test.template.txt"; + +open(IN,$template) or die("Failed to open input file for reading."); +open(OUT, ">>test_bug" . $bug_number . ".xhtml") or die("Failed to open output file for appending."); +while((defined(IN)) && ($line = <IN>)) { + $line =~ s/{BUGNUMBER}/$bug_number/g; + print OUT $line; +} +close(IN); +close(OUT); + +$template = "$FindBin::RealBin/window.template.txt"; + +open(IN,$template) or die("Failed to open input file for reading."); +open(OUT, ">>bug" . $bug_number . "_window.xhtml") or die("Failed to open output file for appending."); +while((defined(IN)) && ($line = <IN>)) { + $line =~ s/{BUGNUMBER}/$bug_number/g; + print OUT $line; +} +close(IN); +close(OUT); + diff --git a/docshell/test/chrome/generic.html b/docshell/test/chrome/generic.html new file mode 100644 index 0000000000..569a78c05a --- /dev/null +++ b/docshell/test/chrome/generic.html @@ -0,0 +1,12 @@ +<html> +<head> + <title> + generic page + </title> + </head> +<body> +<div id="div1" style="height: 1000px; border: thin solid black;"> + A generic page which can be used any time a test needs to load an arbitrary page via http. + </div> +</body> +</html> diff --git a/docshell/test/chrome/mozFrameType_window.xhtml b/docshell/test/chrome/mozFrameType_window.xhtml new file mode 100644 index 0000000000..e5d0126d22 --- /dev/null +++ b/docshell/test/chrome/mozFrameType_window.xhtml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<window title="Test mozFrameType attribute" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTests();"> + + <html:iframe id="normalFrame"/> + <html:iframe id="typeContentFrame" mozframetype="content"/> + + <script type="application/javascript"><![CDATA[ + function runTests() { + let opener = window.arguments[0]; + let SimpleTest = opener.SimpleTest; + + function getDocShellType(frame) { + return frame.contentWindow.docShell.itemType; + } + + var normalFrame = document.getElementById("normalFrame"); + var typeContentFrame = document.getElementById("typeContentFrame"); + + SimpleTest.is(getDocShellType(normalFrame), Ci.nsIDocShellTreeItem.typeChrome, + "normal iframe in chrome document is typeChrome"); + SimpleTest.is(getDocShellType(typeContentFrame), Ci.nsIDocShellTreeItem.typeContent, + "iframe with mozFrameType='content' in chrome document is typeContent"); + + SimpleTest.executeSoon(function () { + // First focus the parent window and then close this one. + SimpleTest.waitForFocus(function() { + let ww = SpecialPowers.Services.ww; + ww.registerNotification(function windowObs(subject, topic, data) { + if (topic == "domwindowclosed") { + ww.unregisterNotification(windowObs); + + // Don't start the next test synchronously! + SimpleTest.executeSoon(function() { + SimpleTest.finish(); + }); + } + }); + + window.close(); + }, opener); + }); + } + ]]></script> +</window> diff --git a/docshell/test/chrome/red.png b/docshell/test/chrome/red.png Binary files differnew file mode 100644 index 0000000000..aa9ce25263 --- /dev/null +++ b/docshell/test/chrome/red.png diff --git a/docshell/test/chrome/test.template.txt b/docshell/test/chrome/test.template.txt new file mode 100644 index 0000000000..b7dd5e5c23 --- /dev/null +++ b/docshell/test/chrome/test.template.txt @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}.xul +--> +<window title="Mozilla Bug {BUGNUMBER}" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Test for Bug {BUGNUMBER}</title> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}"> + Mozilla Bug {BUGNUMBER}</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug {BUGNUMBER} **/ + +SimpleTest.waitForExplicitFinish(); +window.open("bug{BUGNUMBER}_window.xul", "bug{BUGNUMBER}", + "chrome,width=600,height=600"); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_allowContentRetargeting.html b/docshell/test/chrome/test_allowContentRetargeting.html new file mode 100644 index 0000000000..b6b830138f --- /dev/null +++ b/docshell/test/chrome/test_allowContentRetargeting.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runNextTest); + +var TEST_URL = "http://mochi.test:8888/tests/docshell/test/chrome/allowContentRetargeting.sjs"; + +function runNextTest() { + var test = tests.shift(); + if (!test) { + SimpleTest.finish(); + return; + } + test(); +} + +var tests = [ + + // Set allowContentRetargeting = false, load a downloadable URL, verify the + // downloadable stops loading. + function basic() { + var iframe = insertIframe(); + iframe.contentWindow.docShell.allowContentRetargeting = false; + loadIframe(iframe); + }, + + // Set allowContentRetargeting = false on parent docshell, load a downloadable + // URL, verify the downloadable stops loading. + function inherit() { + var docshell = window.docShell; + docshell.allowContentRetargeting = false; + loadIframe(insertIframe()); + }, +]; + +function insertIframe() { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + return iframe; +} + +function loadIframe(iframe) { + iframe.setAttribute("src", TEST_URL); + iframe.contentWindow.docShell. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + addProgressListener(progressListener, + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); +} + +var progressListener = { + onStateChange(webProgress, req, flags, status) { + if (!(flags & Ci.nsIWebProgressListener.STATE_STOP)) + return; + is(Components.isSuccessCode(status), false, + "Downloadable should have failed to load"); + document.querySelector("iframe").remove(); + runNextTest(); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"]), +}; + + </script> +</head> +<body> +<p id="display"> +</p> +</body> +</html> diff --git a/docshell/test/chrome/test_bug112564.xhtml b/docshell/test/chrome/test_bug112564.xhtml new file mode 100644 index 0000000000..83a087b6d9 --- /dev/null +++ b/docshell/test/chrome/test_bug112564.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=112564 +--> +<window title="Mozilla Bug 112564" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=112564">Mozilla Bug 112564</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 112564 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug112564_window.xhtml", "bug112564", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug113934.xhtml b/docshell/test/chrome/test_bug113934.xhtml new file mode 100644 index 0000000000..99b8ae253c --- /dev/null +++ b/docshell/test/chrome/test_bug113934.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=113934 +--> +<window title="Mozilla Bug 113934" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=113934" + target="_blank">Mozilla Bug 396519</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + window.openDialog("bug113934_window.xhtml?content", "bug113934", + "chrome,width=800,height=800,noopener", window); + }); + + ]]></script> +</window> diff --git a/docshell/test/chrome/test_bug215405.xhtml b/docshell/test/chrome/test_bug215405.xhtml new file mode 100644 index 0000000000..e9511a6ed1 --- /dev/null +++ b/docshell/test/chrome/test_bug215405.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=215405 +--> +<window title="Mozilla Bug 215405" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=215405">Mozilla Bug 215405</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 215405 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug215405_window.xhtml", "bug215405", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug293235.xhtml b/docshell/test/chrome/test_bug293235.xhtml new file mode 100644 index 0000000000..4696c37557 --- /dev/null +++ b/docshell/test/chrome/test_bug293235.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=293235.xul +--> +<window title="Mozilla Bug 293235" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=293235"> + Mozilla Bug 293235</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 293235 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug293235_window.xhtml", "bug293235", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug294258.xhtml b/docshell/test/chrome/test_bug294258.xhtml new file mode 100644 index 0000000000..8f61a6c299 --- /dev/null +++ b/docshell/test/chrome/test_bug294258.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=294258.xul +--> +<window title="Mozilla Bug 294258" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=294258"> + Mozilla Bug 294258</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 294258 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug294258_window.xhtml", "bug294258", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug298622.xhtml b/docshell/test/chrome/test_bug298622.xhtml new file mode 100644 index 0000000000..8a154b5145 --- /dev/null +++ b/docshell/test/chrome/test_bug298622.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=298622.xul +--> +<window title="Mozilla Bug 298622" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=298622"> + Mozilla Bug 298622</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 298622 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug298622_window.xhtml", "bug298622", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug301397.xhtml b/docshell/test/chrome/test_bug301397.xhtml new file mode 100644 index 0000000000..3da88ecf53 --- /dev/null +++ b/docshell/test/chrome/test_bug301397.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=301397.xul +--> +<window title="Mozilla Bug 301397" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=301397"> + Mozilla Bug 301397</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 301397 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug301397_window.xhtml", "bug301397", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug303267.xhtml b/docshell/test/chrome/test_bug303267.xhtml new file mode 100644 index 0000000000..03af56d344 --- /dev/null +++ b/docshell/test/chrome/test_bug303267.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=303267.xul +--> +<window title="Mozilla Bug 303267" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=303267">Mozilla Bug 303267</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.expectAssertions(0, 1); + +/** Test for Bug 303267 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug303267_window.xhtml", "bug303267", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug311007.xhtml b/docshell/test/chrome/test_bug311007.xhtml new file mode 100644 index 0000000000..4601c6ec20 --- /dev/null +++ b/docshell/test/chrome/test_bug311007.xhtml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=311007.xul +--> +<window title="Mozilla Bug 311007" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=311007"> + Mozilla Bug 311007</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +if (navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 1); +} + +/** Test for Bug 311007 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug311007_window.xhtml", "bug311007", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug321671.xhtml b/docshell/test/chrome/test_bug321671.xhtml new file mode 100644 index 0000000000..7855db6f13 --- /dev/null +++ b/docshell/test/chrome/test_bug321671.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=321671.xul +--> +<window title="Mozilla Bug 321671" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=321671"> + Mozilla Bug 321671</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 321671 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug321671_window.xhtml", "bug321671", + "chrome,width=600,height=600,scrollbars,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug360511.xhtml b/docshell/test/chrome/test_bug360511.xhtml new file mode 100644 index 0000000000..99b9d180f4 --- /dev/null +++ b/docshell/test/chrome/test_bug360511.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=360511.xul +--> +<window title="Mozilla Bug 360511" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"> + </script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=360511"> + Mozilla Bug 360511</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 360511 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug360511_window.xhtml", "bug360511", + "chrome,scrollbars,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug364461.xhtml b/docshell/test/chrome/test_bug364461.xhtml new file mode 100644 index 0000000000..9bdc016ae7 --- /dev/null +++ b/docshell/test/chrome/test_bug364461.xhtml @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=364461 +--> +<window title="Mozilla Bug 364461" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=364461">Mozilla Bug 364461</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 364461 */ + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({ + "set":[["security.data_uri.block_toplevel_data_uri_navigations", false]] +}, runTests); + +function runTests() { + window.openDialog("bug364461_window.xhtml", "bug364461", + "chrome,width=600,height=600,noopener", window); +} +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug396519.xhtml b/docshell/test/chrome/test_bug396519.xhtml new file mode 100644 index 0000000000..7c99bf4a32 --- /dev/null +++ b/docshell/test/chrome/test_bug396519.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=396519 +--> +<window title="Mozilla Bug 396519" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=396519" + target="_blank">Mozilla Bug 396519</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + + /** Test for Bug 396519 */ + + SimpleTest.waitForExplicitFinish(); + window.openDialog("bug396519_window.xhtml", "bug396519", + "chrome,width=600,height=600,noopener", window); + + ]]></script> +</window> diff --git a/docshell/test/chrome/test_bug396649.xhtml b/docshell/test/chrome/test_bug396649.xhtml new file mode 100644 index 0000000000..8ac05eee85 --- /dev/null +++ b/docshell/test/chrome/test_bug396649.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=396649.xul +--> +<window title="Mozilla Bug 396649" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src= + "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"> + </script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=396649"> + Mozilla Bug 396649</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 396649 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug396649_window.xhtml", "bug396649", + "chrome,width=600,height=600,scrollbars,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug428288.html b/docshell/test/chrome/test_bug428288.html new file mode 100644 index 0000000000..4697aed515 --- /dev/null +++ b/docshell/test/chrome/test_bug428288.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=428288 +--> +<head> + <title>Test for Bug 428288</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=428288">Mozilla Bug 428288</a> +<p id="display"></p> +<div id="content" style="display: none"> + <iframe name="target"></iframe> + <a id="crashy" target="target" href="about:blank">crash me</a> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 428288 */ + +function makeClick() { + var event = document.createEvent("MouseEvents"); + event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null); + document.getElementById("crashy").dispatchEvent(event); + return true; +} + +ok(makeClick(), "Crashes if bug 428288 is present"); + +</script> +</pre> +</body> +</html> + diff --git a/docshell/test/chrome/test_bug449778.xhtml b/docshell/test/chrome/test_bug449778.xhtml new file mode 100644 index 0000000000..67e17164ea --- /dev/null +++ b/docshell/test/chrome/test_bug449778.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=449778 +--> +<window title="Mozilla Bug 449778" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=449778" + target="_blank">Mozilla Bug 396519</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + window.openDialog("bug449778_window.xhtml", "bug449778", + "chrome,width=800,height=800,noopener", window); + }); + + ]]></script> +</window> diff --git a/docshell/test/chrome/test_bug449780.xhtml b/docshell/test/chrome/test_bug449780.xhtml new file mode 100644 index 0000000000..43ed3ce25d --- /dev/null +++ b/docshell/test/chrome/test_bug449780.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=449780 +--> +<window title="Mozilla Bug 449780" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=449780" + target="_blank">Mozilla Bug 396519</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + window.openDialog("bug449780_window.xhtml", "bug449780", + "chrome,width=800,height=800,noopener", window); + }); + + ]]></script> +</window> diff --git a/docshell/test/chrome/test_bug453650.xhtml b/docshell/test/chrome/test_bug453650.xhtml new file mode 100644 index 0000000000..2283e29206 --- /dev/null +++ b/docshell/test/chrome/test_bug453650.xhtml @@ -0,0 +1,120 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=453650 +--> +<window title="Mozilla Bug 453650" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 453650 */ + SimpleTest.waitForExplicitFinish(); + + var iter = runTests(); + nextTest(); + + function* runTests() { + var iframe = document.createXULElement("iframe"); + iframe.style.width = "300px"; + iframe.style.height = "300px"; + iframe.setAttribute("src", "data:text/html,<h1 id='h'>hello</h1>"); + + document.documentElement.appendChild(iframe); + yield whenLoaded(iframe); + info("iframe loaded"); + + var h1 = iframe.contentDocument.getElementById("h"); + let myCallback = function() { h1.style.width = "400px"; }; + info("Calling waitForInterruptibleReflow"); + yield waitForInterruptibleReflow(iframe.docShell, myCallback); + info("got past top-level waitForInterruptibleReflow"); + + myCallback = function() { h1.style.width = "300px"; }; + info("Calling waitForReflow"); + waitForReflow(iframe.docShell, myCallback); + info("got past top-level waitForReflow"); + yield is(300, h1.offsetWidth, "h1 has correct width"); + + SimpleTest.finish(); + } + + function waitForInterruptibleReflow(docShell, + callbackThatShouldTriggerReflow) { + waitForReflow(docShell, callbackThatShouldTriggerReflow, true); + } + + function waitForReflow(docShell, callbackThatShouldTriggerReflow, + interruptible = false) { + info("Entering waitForReflow"); + function done() { + info("Entering done (inside of waitForReflow)"); + + docShell.removeWeakReflowObserver(observer); + SimpleTest.executeSoon(nextTest); + } + + var observer = { + reflow (start, end) { + info("Entering observer.reflow"); + if (interruptible) { + ok(false, "expected interruptible reflow"); + } else { + ok(true, "observed uninterruptible reflow"); + } + + info("times: " + start + ", " + end); + ok(start <= end, "reflow start time lower than end time"); + done(); + }, + + reflowInterruptible (start, end) { + info("Entering observer.reflowInterruptible"); + if (!interruptible) { + ok(false, "expected uninterruptible reflow"); + } else { + ok(true, "observed interruptible reflow"); + } + + info("times: " + start + ", " + end); + ok(start <= end, "reflow start time lower than end time"); + done(); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIReflowObserver", + "nsISupportsWeakReference", + ]), + }; + + info("waitForReflow is adding a reflow observer"); + docShell.addWeakReflowObserver(observer); + callbackThatShouldTriggerReflow(); + } + + function whenLoaded(iframe) { + info("entering whenLoaded"); + iframe.addEventListener("load", function() { + SimpleTest.executeSoon(nextTest); + }, { once: true }); + } + + function nextTest() { + info("entering nextTest"); + iter.next(); + } + + ]]> + </script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=453650" + target="_blank">Mozilla Bug 453650</a> + </body> +</window> diff --git a/docshell/test/chrome/test_bug454235.xhtml b/docshell/test/chrome/test_bug454235.xhtml new file mode 100644 index 0000000000..d64f701f8e --- /dev/null +++ b/docshell/test/chrome/test_bug454235.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=454235 +--> +<window title="Mozilla Bug 454235" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=454235" + target="_blank">Mozilla Bug 454235</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + + /** Test for Bug 454235 */ +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(doTest); + +function doTest() { + var hiddenBrowser = document.getElementById("hiddenBrowser"); + + hiddenBrowser.contentWindow.focus(); + ok(!hiddenBrowser.contentDocument.hasFocus(), "hidden browser is unfocusable"); + + SimpleTest.finish(); +} + + + + ]]></script> + <box flex="1" style="visibility: hidden; border:5px black solid"> + <browser style="border:5px blue solid" id="hiddenBrowser" src="bug454235-subframe.xhtml"/> + </box> +</window> diff --git a/docshell/test/chrome/test_bug456980.xhtml b/docshell/test/chrome/test_bug456980.xhtml new file mode 100644 index 0000000000..d1741209c6 --- /dev/null +++ b/docshell/test/chrome/test_bug456980.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=456980 +--> +<window title="Mozilla Bug 456980" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=456980" + target="_blank">Mozilla Bug 396519</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + window.openDialog("bug113934_window.xhtml?chrome", "bug456980", + "chrome,width=800,height=800,noopener", window); + }); + + ]]></script> +</window> diff --git a/docshell/test/chrome/test_bug565388.xhtml b/docshell/test/chrome/test_bug565388.xhtml new file mode 100644 index 0000000000..23af2c146d --- /dev/null +++ b/docshell/test/chrome/test_bug565388.xhtml @@ -0,0 +1,79 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=565388 +--> +<window title="Mozilla Bug 565388" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 565388 */ + SimpleTest.waitForExplicitFinish(); + +function test() { + var progressListener = { + add(docShell, callback) { + this.callback = callback; + this.docShell = docShell; + docShell. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW); + }, + + finish() { + this.docShell. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + removeProgressListener(this); + this.callback(); + }, + + onStateChange (webProgress, req, flags, status) { + if (req.name.startsWith("data:application/vnd.mozilla.xul")) { + if (flags & Ci.nsIWebProgressListener.STATE_STOP) + this.finish(); + } + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + } + + var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] + .getService(Ci.nsIPrincipal); + var webNav = SpecialPowers.Services.appShell.createWindowlessBrowser(true); + var docShell = webNav.docShell; + docShell.createAboutBlankContentViewer(systemPrincipal, systemPrincipal); + var win = docShell.contentViewer.DOMDocument.defaultView; + + progressListener.add(docShell, function(){ + is(win.document.documentURI, "data:application/xhtml+xml;charset=utf-8,<window/>"); + webNav.close(); + SimpleTest.finish(); + }); + + win.location = "data:application/xhtml+xml;charset=utf-8,<window/>"; +} + +addLoadEvent(function onLoad() { + test(); +}); + + ]]> + </script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=565388" + target="_blank">Mozilla Bug 565388</a> + </body> +</window> diff --git a/docshell/test/chrome/test_bug582176.xhtml b/docshell/test/chrome/test_bug582176.xhtml new file mode 100644 index 0000000000..5f189ae87b --- /dev/null +++ b/docshell/test/chrome/test_bug582176.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=582176.xul +--> +<window title="Mozilla Bug 582176" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=582176"> + Mozilla Bug 582176</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 582176 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug582176_window.xhtml", "bug582176", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug608669.xhtml b/docshell/test/chrome/test_bug608669.xhtml new file mode 100644 index 0000000000..f6a62b0802 --- /dev/null +++ b/docshell/test/chrome/test_bug608669.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=608669 +--> +<window title="Mozilla Bug 608669" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=608669" + target="_blank">Mozilla Bug 608669</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 608669 */ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(nextTest); + +let gen = doTest(); + +function nextTest() { + gen.next(); +} + +let chromeWindow = window.browsingContext.topChromeWindow; + +function* doTest() { + var notificationCount = 0; + var observer = { + observe(aSubject, aTopic, aData) { + is(aTopic, "chrome-document-global-created", + "correct topic"); + is(aData, "null", + "correct data"); + notificationCount++; + } + }; + + var os = SpecialPowers.Services.obs; + os.addObserver(observer, "chrome-document-global-created"); + os.addObserver(observer, "content-document-global-created"); + + is(notificationCount, 0, "initial count"); + + // create a new window + var testWin = chromeWindow.open("", "bug 608669", "chrome,width=600,height=600"); + testWin.x = "y"; + is(notificationCount, 1, "after created window"); + + // Try loading in the window + testWin.location = "bug608669.xhtml"; + chromeWindow.onmessage = nextTest; + yield undefined; + is(notificationCount, 1, "after first load"); + is(testWin.x, "y", "reused window"); + + // Try loading again in the window + testWin.location = "bug608669.xhtml?x"; + chromeWindow.onmessage = nextTest; + yield undefined; + is(notificationCount, 2, "after second load"); + is("x" in testWin, false, "didn't reuse window"); + + chromeWindow.onmessage = null; + + testWin.close(); + + os.removeObserver(observer, "chrome-document-global-created"); + os.removeObserver(observer, "content-document-global-created"); + SimpleTest.finish(); +} + + ]]></script> +</window> diff --git a/docshell/test/chrome/test_bug662200.xhtml b/docshell/test/chrome/test_bug662200.xhtml new file mode 100644 index 0000000000..19a386ba97 --- /dev/null +++ b/docshell/test/chrome/test_bug662200.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=662200.xul +--> +<window title="Mozilla Bug 662200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=662200"> + Mozilla Bug 662200</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 662200 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug662200_window.xhtml", "bug662200", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug690056.xhtml b/docshell/test/chrome/test_bug690056.xhtml new file mode 100644 index 0000000000..5f7a2b9534 --- /dev/null +++ b/docshell/test/chrome/test_bug690056.xhtml @@ -0,0 +1,26 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=690056 +--> +<window title="Mozilla Bug 690056" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=690056" + target="_blank">Mozilla Bug 690056</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 690056 */ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug690056_window.xhtml", "bug690056", + "chrome,width=600,height=600,noopener", window); + ]]> + </script> +</window> diff --git a/docshell/test/chrome/test_bug789773.xhtml b/docshell/test/chrome/test_bug789773.xhtml new file mode 100644 index 0000000000..0f4a67fc10 --- /dev/null +++ b/docshell/test/chrome/test_bug789773.xhtml @@ -0,0 +1,67 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=789773 +--> +<window title="Mozilla Bug 789773" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=789773" + target="_blank">Mozilla Bug 789773</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + /* Test for Bug 789773. + * + * See comment 50 for the situation we're testing against here. + * + * Note that the failure mode of this test is to hang, and hang the browser on quit. + * This is an unfortunate occurance, but that's why we're testing it. + */ + SimpleTest.waitForExplicitFinish(); + + const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + + var calledListenerForBrowserChromeURL = false; + var testProgressListener = { + START_DOC: Ci.nsIWebProgressListener.STATE_START | Ci.nsIWebProgressListener.STATE_IS_DOCUMENT, + onStateChange(wp, req, stateFlags, status) { + let browserChromeFileName = AppConstants.BROWSER_CHROME_URL.split("/").reverse()[0]; + if (req.name.includes(browserChromeFileName)) { + wp.DOMWindow; // Force the lazy creation of a DOM window. + calledListenerForBrowserChromeURL = true; + } + if (req.name.includes("mozilla.html") && (stateFlags & Ci.nsIWebProgressListener.STATE_STOP)) + finishTest(); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + } + + // Add our progress listener + var webProgress = Cc['@mozilla.org/docloaderservice;1'].getService(Ci.nsIWebProgress); + webProgress.addProgressListener(testProgressListener, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST); + + // Open the window. + var popup = window.open("about:mozilla", "_blank", "width=640,height=400"); + + // Wait for the window to load. + function finishTest() { + webProgress.removeProgressListener(testProgressListener); + ok(true, "Loaded the popup window without spinning forever in the event loop!"); + ok(calledListenerForBrowserChromeURL, "Should have called the progress listener for browser.xhtml"); + popup.close(); + SimpleTest.finish(); + } + + ]]> + </script> +</window> diff --git a/docshell/test/chrome/test_bug846906.xhtml b/docshell/test/chrome/test_bug846906.xhtml new file mode 100644 index 0000000000..74bfdf7036 --- /dev/null +++ b/docshell/test/chrome/test_bug846906.xhtml @@ -0,0 +1,94 @@ +<?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"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=846906 +--> +<window title="Mozilla Bug 846906" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 846906 */ + SimpleTest.waitForExplicitFinish(); + + var appShellService = SpecialPowers.Services.appShell; + ok(appShellService, "Should be able to get app shell service"); + + var windowlessBrowser = appShellService.createWindowlessBrowser(); + ok(windowlessBrowser, "Should be able to create windowless browser"); + + ok(windowlessBrowser instanceof Ci.nsIWindowlessBrowser, + "Windowless browser should implement nsIWindowlessBrowser"); + + var webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation); + ok(webNavigation, "Windowless browser should implement nsIWebNavigation"); + + var interfaceRequestor = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor); + ok(interfaceRequestor, "Should be able to query interface requestor interface"); + + var docShell = windowlessBrowser.docShell; + ok(docShell, "Should be able to get doc shell interface"); + + var document = webNavigation.document; + ok(document, "Should be able to get document"); + + var iframe = document.createXULElement("iframe"); + ok(iframe, "Should be able to create iframe"); + + iframe.onload = function () { + ok(true, "Should receive initial onload event"); + + iframe.onload = function () { + ok(true, "Should receive onload event"); + + var contentDocument = iframe.contentDocument; + ok(contentDocument, "Should be able to get content document"); + + var div = contentDocument.getElementById("div1"); + ok(div, "Should be able to get element by id"); + + var rect = div.getBoundingClientRect(); + ok(rect, "Should be able to get bounding client rect"); + + // xxx: can we do better than hardcoding these values here? + is(rect.width, 1024); + is(rect.height, 768); + + windowlessBrowser.close(); + + // Once the browser is closed, nsIWebNavigation and + // nsIInterfaceRequestor methods should no longer be accessible. + try { + windowlessBrowser.getInterface(Ci.nsIDocShell); + ok(false); + } catch (e) { + is(e.result, Cr.NS_ERROR_NULL_POINTER); + } + + try { + windowlessBrowser.document; + ok(false); + } catch (e) { + is(e.result, Cr.NS_ERROR_NULL_POINTER); + } + + SimpleTest.finish(); + }; + iframe.setAttribute("src", "http://mochi.test:8888/chrome/docshell/test/chrome/bug846906.html"); + }; + document.documentElement.appendChild(iframe); + + ]]> + </script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=846906" + target="_blank">Mozilla Bug 846906</a> + </body> +</window> diff --git a/docshell/test/chrome/test_bug89419.xhtml b/docshell/test/chrome/test_bug89419.xhtml new file mode 100644 index 0000000000..848982180f --- /dev/null +++ b/docshell/test/chrome/test_bug89419.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=89419.xul +--> +<window title="Mozilla Bug 89419" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=89419"> + Mozilla Bug 89419</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 89419 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug89419_window.xhtml", "bug89419", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_bug909218.html b/docshell/test/chrome/test_bug909218.html new file mode 100644 index 0000000000..bcbcc176eb --- /dev/null +++ b/docshell/test/chrome/test_bug909218.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(test); + +// The default flags we will stick on the docShell - every request made by the +// docShell should include those flags. +const TEST_FLAGS = Ci.nsIRequest.LOAD_ANONYMOUS | + Ci.nsIRequest.LOAD_BYPASS_CACHE | + Ci.nsIRequest.INHIBIT_CACHING; + +var TEST_URL = "http://mochi.test:8888/chrome/docshell/test/chrome/bug909218.html"; + +// These are the requests we expect to see loading TEST_URL into our iframe. + +// The test entry-point. The basic outline is: +// * Create an iframe and set defaultLoadFlags on its docShell. +// * Add a web progress listener to observe each request as the iframe is +// loaded, and check that each request has the flags we specified. +// * Load our test URL into the iframe and wait for the load to complete. +function test() { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + var docShell = iframe.contentWindow.docShell; + // Add our progress listener - when it notices the top-level document is + // complete, the test will end. + RequestWatcher.init(docShell, SimpleTest.finish); + // Set the flags we care about, then load our test URL. + docShell.defaultLoadFlags = TEST_FLAGS; + iframe.setAttribute("src", TEST_URL); +} + +// an nsIWebProgressListener that checks all requests made by the docShell +// have the flags we expect. +var RequestWatcher = { + init(docShell, callback) { + this.callback = callback; + this.docShell = docShell; + docShell. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST | + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); + // These are the requests we expect to see - initialize each to have a + // count of zero. + this.requestCounts = {}; + for (var url of [ + TEST_URL, + // content loaded by the above test html. + "http://mochi.test:8888/chrome/docshell/test/chrome/bug909218.js", + "http://mochi.test:8888/tests/SimpleTest/test.css", + "http://mochi.test:8888/tests/docshell/test/chrome/red.png", + // the content of an iframe in the test html. + "http://mochi.test:8888/chrome/docshell/test/chrome/generic.html", + ]) { + this.requestCounts[url] = 0; + } + }, + + // Finalize the test after we detect a completed load. We check we saw the + // correct requests and make a callback to exit. + finalize() { + ok(Object.keys(this.requestCounts).length, "we expected some requests"); + for (var url in this.requestCounts) { + var count = this.requestCounts[url]; + // As we are looking at all request states, we expect more than 1 for + // each URL - 0 or 1 would imply something went wrong - >1 just means + // multiple states for each request were recorded, which we don't care + // about (we do care they all have the correct flags though - but we + // do that in onStateChange) + ok(count > 1, url + " saw " + count + " requests"); + } + this.docShell. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + removeProgressListener(this); + this.callback(); + }, + + onStateChange(webProgress, req, flags, status) { + // We are checking requests - if there isn't one, ignore it. + if (!req) { + return; + } + // We will usually see requests for 'about:document-onload-blocker' not + // have the flag, so we just ignore them. + // We also see, eg, resource://gre-resources/loading-image.png, so + // skip resource:// URLs too. + // We may also see, eg, chrome://global/skin/icons/chevron.svg, so + // skip chrome:// URLs too. + if (req.name.startsWith("about:") || req.name.startsWith("resource:") || + req.name.startsWith("chrome:") || req.name.startsWith("documentchannel:")) { + return; + } + is(req.loadFlags & TEST_FLAGS, TEST_FLAGS, "request " + req.name + " has the expected flags"); + this.requestCounts[req.name] += 1; + var stopFlags = Ci.nsIWebProgressListener.STATE_STOP | + Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + if (req.name == TEST_URL && (flags & stopFlags) == stopFlags) { + this.finalize(); + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +</script> +</head> +</html> diff --git a/docshell/test/chrome/test_bug92598.xhtml b/docshell/test/chrome/test_bug92598.xhtml new file mode 100644 index 0000000000..1b89e3eeb3 --- /dev/null +++ b/docshell/test/chrome/test_bug92598.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=92598 +--> +<window title="Mozilla Bug 92598" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=92598">Mozilla Bug 92598</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 92598 */ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug92598_window.xhtml", "bug92598", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_docRedirect.sjs b/docshell/test/chrome/test_docRedirect.sjs new file mode 100644 index 0000000000..4050eb06d7 --- /dev/null +++ b/docshell/test/chrome/test_docRedirect.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + response.setHeader("Location", "http://example.org/"); + response.write("Hello world!"); +} diff --git a/docshell/test/chrome/test_docRedirect.xhtml b/docshell/test/chrome/test_docRedirect.xhtml new file mode 100644 index 0000000000..1688d9823e --- /dev/null +++ b/docshell/test/chrome/test_docRedirect.xhtml @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1342989 +--> +<window title="Mozilla Bug 1342989" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + + const WEB_PROGRESS_LISTENER_FLAGS = + Object.keys(Ci.nsIWebProgressListener).filter( + propName => propName.startsWith("STATE_") + ); + + function bitFlagsToNames(flags, knownNames, intf) { + return knownNames.map( (F) => { + return (flags & intf[F]) ? F : undefined; + }).filter( (s) => !!s ); + } + + var progressListener = { + add(docShell, callback) { + this.callback = callback; + this.docShell = docShell; + docShell. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_ALL); + }, + + finish(success) { + this.docShell. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + removeProgressListener(this); + this.callback(success); + }, + + onStateChange (webProgress, req, flags, status) { + if (!(flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT) && + !(flags & Ci.nsIWebProgressListener.STATE_IS_REDIRECTED_DOCUMENT)) + return; + + var channel = req.QueryInterface(Ci.nsIChannel); + + if (flags & Ci.nsIWebProgressListener.STATE_IS_REDIRECTED_DOCUMENT) { + SimpleTest.is(channel.URI.host, "example.org", + "Should be redirected to example.org (see test_docRedirect.sjs)"); + this.finish(true); + } + + // Fail in case we didn't receive document redirection event. + if (flags & Ci.nsIWebProgressListener.STATE_STOP) + this.finish(false); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + } + + var webNav = SpecialPowers.Services.appShell.createWindowlessBrowser(true); + let docShell = webNav.docShell; + let system = Cc["@mozilla.org/systemprincipal;1"].getService(Ci.nsIPrincipal); + docShell.createAboutBlankContentViewer(system, system); + + progressListener.add(docShell, function(success) { + webNav.close(); + SimpleTest.is(success, true, "Received document redirect event"); + SimpleTest.finish(); + }); + + var win = docShell.contentViewer.DOMDocument.defaultView; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + win.location = "http://example.com/chrome/docshell/test/chrome/test_docRedirect.sjs" + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1342989" + target="_blank">Mozilla Bug 1342989</a> + </body> +</window> diff --git a/docshell/test/chrome/test_mozFrameType.xhtml b/docshell/test/chrome/test_mozFrameType.xhtml new file mode 100644 index 0000000000..f9740a761b --- /dev/null +++ b/docshell/test/chrome/test_mozFrameType.xhtml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=769771 +--> +<window title="Test mozFrameType attribute" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +if (navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 1); +} + +/** Test for Bug 769771 */ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function () { + window.openDialog("mozFrameType_window.xhtml", "mozFrameType", + "chrome,width=600,height=600,noopener", window); +}); + +]]> +</script> + +</window> diff --git a/docshell/test/chrome/test_open_and_immediately_close_opener.html b/docshell/test/chrome/test_open_and_immediately_close_opener.html new file mode 100644 index 0000000000..bb5f6f054d --- /dev/null +++ b/docshell/test/chrome/test_open_and_immediately_close_opener.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for Bug 1702678</title> + <meta charset="utf-8"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1702678">Mozilla Bug 1702678</a> + +<script type="application/javascript"> +"use strict"; + +const HTML = ` +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <script> + // We need to queue a promise reaction job whilch will close the window + // during the nested event loop spun up by the window opening code. + Promise.resolve().then(() => { + window.close(); + }); + window.open("data:text/html,Hello"); + <\/script> +</head> +</html> +`; + +add_task(async function() { + // This bug only manifests when opening tabs in new windows. + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", 2]], + }); + + // Create a window in a new BrowsingContextGroup so that it will be the last + // window in the group when it closes, and the group will be destroyed. + window.open(`data:text/html,${encodeURIComponent(HTML)}`, "", "noopener"); + + // Make a few trips through the event loop to ensure we've had a chance to + // open and close the relevant windows. + for (let i = 0; i < 10; i++) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + + ok(true, "We didn't crash"); +}); +</script> + +</body> +</html> + diff --git a/docshell/test/chrome/test_viewsource_forbidden_in_iframe.xhtml b/docshell/test/chrome/test_viewsource_forbidden_in_iframe.xhtml new file mode 100644 index 0000000000..0cc45c7821 --- /dev/null +++ b/docshell/test/chrome/test_viewsource_forbidden_in_iframe.xhtml @@ -0,0 +1,159 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=624883 +--> +<window title="Mozilla Bug 624883" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=624883" + target="_blank">Mozilla Bug 624883</a> + </body> + + <!-- test code goes here --> + <iframe type="content" onload="startTest()" src="file_viewsource_forbidden_in_iframe.html"></iframe> + + <script type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + // We create a promise that will resolve with the error message + // on a network error page load and reject on any other load. + function createNetworkErrorMessagePromise(frame) { + return new Promise(function(resolve, reject) { + + // Error pages do not fire "load" events, so use a progressListener. + var originalDocumentURI = frame.contentDocument.documentURI; + var progressListener = { + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + // Make sure nothing other than an error page is loaded. + if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE)) { + reject("location change was not to an error page"); + } + }, + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + // Wait until the documentURI changes (from about:blank) this should + // be the error page URI. + var documentURI = frame.contentDocument.documentURI; + if (documentURI == originalDocumentURI) { + return; + } + + aWebProgress.removeProgressListener(progressListener, + Ci.nsIWebProgress.NOTIFY_ALL); + var matchArray = /about:neterror\?.*&d=([^&]*)/.exec(documentURI); + if (!matchArray) { + reject("no network error message found in URI") + return; + } + + var errorMsg = matchArray[1]; + resolve(decodeURIComponent(errorMsg)); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", + "nsISupportsWeakReference"]) + }; + + frame.contentWindow.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress) + .addProgressListener(progressListener, + Ci.nsIWebProgress.NOTIFY_LOCATION | + Ci.nsIWebProgress.NOTIFY_STATE_REQUEST); + }); + } + + function startTest() { + // Get a reference message that we know will be an unknown protocol message, + // so we can use it for comparisons in the test cases. + var refIframe = window[0].document.getElementById("refIframe"); + var refErrorPromise = createNetworkErrorMessagePromise(refIframe); + + refErrorPromise.then( + function(msg) { + window.refErrorMsg = msg; + var testIframe = window[0].document.getElementById("testIframe"); + + // Run test cases on load of "about:blank", so that the URI always changes + // and we can detect this in our Promise. + testIframe.onload = runNextTestCase; + testIframe.src = "about:blank"; + }, + function(reason) { + ok(false, "Could not get reference error message", reason); + SimpleTest.finish(); + }) + .catch(function(e) { + ok(false, "Unexpected exception thrown getting reference error message", e); + }); + + refIframe.src = "wibble://example.com"; + } + + function runTestCase(testCase) { + var testIframe = window[0].document.getElementById("testIframe"); + var expectedErrorMsg = window.refErrorMsg.replace("wibble", testCase.expectedProtocolList); + + var testErrorPromise = createNetworkErrorMessagePromise(testIframe); + testErrorPromise.then( + function(actualErrorMsg) { + is(actualErrorMsg, expectedErrorMsg, testCase.desc); + testIframe.src = "about:blank"; + }, + function(reason) { + ok(false, testCase.desc, reason); + testIframe.src = "about:blank"; + }) + .catch(function(e) { + ok(false, testCase.desc + " - unexpected exception thrown", e); + }); + + testIframe.src = testCase.protocols + "://example.com/!/"; + } + + var testCaseIndex = -1; + let testCases = [ + { + desc: "Test 1: view-source should not be allowed in an iframe", + protocols: "view-source:http", + expectedProtocolList: "view-source, http" + }, + { + desc: "Test 2: jar:view-source should not be allowed in an iframe", + protocols: "jar:view-source:http", + expectedProtocolList: "jar, view-source, http" + }, + { + desc: "Test 3: if invalid protocol first should report before view-source", + protocols: "wibble:view-source:http", + // Nothing after the invalid protocol gets set as a proper nested URI, + // so the list stops there. + expectedProtocolList: "wibble" + }, + { + desc: "Test 4: if view-source first should report before invalid protocol", + protocols: "view-source:wibble:http", + expectedProtocolList: "view-source, wibble" + } + ]; + + function runNextTestCase() { + ++testCaseIndex; + if (testCaseIndex == testCases.length) { + SimpleTest.finish(); + return; + } + + runTestCase(testCases[testCaseIndex]); + } + + ]]> + </script> +</window> diff --git a/docshell/test/chrome/window.template.txt b/docshell/test/chrome/window.template.txt new file mode 100644 index 0000000000..4c520dc075 --- /dev/null +++ b/docshell/test/chrome/window.template.txt @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="{BUGNUMBER}Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="setTimeout(nextTest,0);" + title="bug {BUGNUMBER} test"> + + <script type="application/javascript" + src="docshell_helpers.js"> + </script> + + <script type="application/javascript"><![CDATA[ + + // Define the generator-iterator for the tests. + var tests = testIterator(); + + //// + // Execute the next test in the generator function. + // + function nextTest() { + tests.next(); + } + + //// + // Generator function for test steps for bug {BUGNUMBER}: + // Description goes here. + // + function testIterator() + { + // Test steps go here. See bug303267_window.xhtml for an example. + + // Tell the framework the test is finished. Include the final 'yield' + // statement to prevent a StopIteration exception from being thrown. + finish(); + yield undefined; + } + + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" src="about:blank"/> +</window> diff --git a/docshell/test/iframesandbox/file_child_navigation_by_location.html b/docshell/test/iframesandbox/file_child_navigation_by_location.html new file mode 100644 index 0000000000..7365bed81f --- /dev/null +++ b/docshell/test/iframesandbox/file_child_navigation_by_location.html @@ -0,0 +1 @@ +<script>function onNav() { parent.parent.postMessage("childIframe", "*"); } window.onload = onNav; window.onhashchange = onNav;</script> diff --git a/docshell/test/iframesandbox/file_marquee_event_handlers.html b/docshell/test/iframesandbox/file_marquee_event_handlers.html new file mode 100644 index 0000000000..13ee31ddb7 --- /dev/null +++ b/docshell/test/iframesandbox/file_marquee_event_handlers.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test marquee attribute event handlers in iframe sandbox</title> +</head> +<body> + <!-- Note that the width here is slightly longer than the contents, to make + sure we bounce and finish very quickly. --> + <marquee loop="2" width="145" behavior="alternate" truespeed scrolldelay="1" + onstart="parent.postMessage(window.name + ' marquee onstart', '*');" + onbounce="parent.postMessage(window.name + ' marquee onbounce', '*');" + onfinish="parent.postMessage(window.name + ' marquee onfinish', '*');"> + Will bounce and finish + </marquee> +</body> +</html> diff --git a/docshell/test/iframesandbox/file_other_auxiliary_navigation_by_location.html b/docshell/test/iframesandbox/file_other_auxiliary_navigation_by_location.html new file mode 100644 index 0000000000..ad24c0f242 --- /dev/null +++ b/docshell/test/iframesandbox/file_other_auxiliary_navigation_by_location.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test window for other auxiliary navigation by location tests</title> +<script> + function onNav() { + opener.postMessage(window.name, "*"); + } + + window.onload = onNav; + window.onhashchange = onNav; +</script> +</head> +</html> diff --git a/docshell/test/iframesandbox/file_our_auxiliary_navigation_by_location.html b/docshell/test/iframesandbox/file_our_auxiliary_navigation_by_location.html new file mode 100644 index 0000000000..978980df25 --- /dev/null +++ b/docshell/test/iframesandbox/file_our_auxiliary_navigation_by_location.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test window for our auxiliary navigation by location tests</title> +<script> + function onNav() { + opener.parent.postMessage(window.name, "*"); + } + + window.onload = onNav; + window.onhashchange = onNav; +</script> +</head> +</html> diff --git a/docshell/test/iframesandbox/file_parent_navigation_by_location.html b/docshell/test/iframesandbox/file_parent_navigation_by_location.html new file mode 100644 index 0000000000..9a2e95fad0 --- /dev/null +++ b/docshell/test/iframesandbox/file_parent_navigation_by_location.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test window for parent navigation by location tests</title> +<script> + function onNav() { + parent.postMessage(window.name, "*"); + } + + window.onload = onNav; + window.onhashchange = onNav; +</script> +</head> +<body> + <iframe name="childIframe" sandbox="allow-scripts allow-same-origin allow-top-navigation"></iframe> +</body> +</html> diff --git a/docshell/test/iframesandbox/file_sibling_navigation_by_location.html b/docshell/test/iframesandbox/file_sibling_navigation_by_location.html new file mode 100644 index 0000000000..51a52bb8eb --- /dev/null +++ b/docshell/test/iframesandbox/file_sibling_navigation_by_location.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test window for sibling navigation by location tests</title> +<script> + function onNav() { + parent.postMessage(window.name, "*"); + } + + window.onload = onNav; + window.onhashchange = onNav; +</script> +</head> +</html> diff --git a/docshell/test/iframesandbox/file_top_navigation_by_location.html b/docshell/test/iframesandbox/file_top_navigation_by_location.html new file mode 100644 index 0000000000..194430f38c --- /dev/null +++ b/docshell/test/iframesandbox/file_top_navigation_by_location.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test window for top navigation by location tests</title> +<script> + function onNav() { + opener.postMessage(window.name, "*"); + } + + window.onload = onNav; + window.onhashchange = onNav; +</script> +</head> +<body> + <iframe name="if1" sandbox="allow-scripts allow-same-origin"></iframe> + <iframe name="if2" sandbox="allow-scripts allow-same-origin allow-top-navigation"></iframe> + <iframe name="if3" sandbox="allow-scripts allow-top-navigation"></iframe> +</body> +</html> diff --git a/docshell/test/iframesandbox/file_top_navigation_by_location_exotic.html b/docshell/test/iframesandbox/file_top_navigation_by_location_exotic.html new file mode 100644 index 0000000000..9a26bc80db --- /dev/null +++ b/docshell/test/iframesandbox/file_top_navigation_by_location_exotic.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test window for top navigation by location tests</title> +<script> + function onBlock() { + opener.postMessage({ name: window.name, blocked: true }, "*"); + } + + function onNav() { + opener.postMessage({ name: window.name, blocked: false }, "*"); + } + + function setOwnHref() { + // eslint-disable-next-line no-self-assign + location.href = location.href; + } + + window.onload = onNav; +</script> +</head> +<body> + <iframe name="if1" sandbox="allow-scripts allow-same-origin"></iframe> + <iframe name="if2" sandbox="allow-scripts allow-same-origin allow-top-navigation"></iframe> +</body> +</html> diff --git a/docshell/test/iframesandbox/file_top_navigation_by_user_activation.html b/docshell/test/iframesandbox/file_top_navigation_by_user_activation.html new file mode 100644 index 0000000000..8454f1fac4 --- /dev/null +++ b/docshell/test/iframesandbox/file_top_navigation_by_user_activation.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test window for top navigation with user activation</title> +<script> +window.onload = () => { + opener.postMessage("READY", "*"); +}; + +window.onhashchange = () => { + opener.postMessage("NAVIGATED", "*"); +}; + +window.onmessage = (e) => { + if (e.data == "CLICK" || e.data == "SCRIPT") { + frames[0].postMessage([e.data, location.href + "#hash"], "*"); + } else { + opener.postMessage(e.data, "*"); + } +}; +</script> +</head> +<body> + <iframe sandbox="allow-scripts allow-top-navigation-by-user-activation" src="http://example.org/tests/docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html"></iframe> +</body> +</html> diff --git a/docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html b/docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html new file mode 100644 index 0000000000..b775579f28 --- /dev/null +++ b/docshell/test/iframesandbox/file_top_navigation_by_user_activation_iframe.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<title>Test window for top navigation with user activation</title> +<script> +function navigate(aURL) { + try { + top.location.href = aURL; + } catch (e) { + top.postMessage("BLOCKED", "*"); + } +} + +window.onmessage = (e) => { + SpecialPowers.wrap(document).clearUserGestureActivation(); + let [command, url] = e.data; + if (command == "CLICK") { + let button = document.querySelector("button"); + button.addEventListener("click", () => { + navigate(url); + }, { once: true }); + synthesizeMouseAtCenter(button, {}); + } else if (command == "SCRIPT") { + navigate(url); + } +}; +</script> +</head> +<body><button>Click</button></body> +</html> diff --git a/docshell/test/iframesandbox/mochitest.ini b/docshell/test/iframesandbox/mochitest.ini new file mode 100644 index 0000000000..ee5a668fcc --- /dev/null +++ b/docshell/test/iframesandbox/mochitest.ini @@ -0,0 +1,30 @@ +[DEFAULT] +support-files = + file_child_navigation_by_location.html + file_marquee_event_handlers.html + file_other_auxiliary_navigation_by_location.html + file_our_auxiliary_navigation_by_location.html + file_parent_navigation_by_location.html + file_sibling_navigation_by_location.html + file_top_navigation_by_location.html + file_top_navigation_by_location_exotic.html + +[test_child_navigation_by_location.html] +[test_marquee_event_handlers.html] +skip-if = true # Bug 1455996 +[test_other_auxiliary_navigation_by_location.html] +tags = openwindow +[test_our_auxiliary_navigation_by_location.html] +tags = openwindow +[test_parent_navigation_by_location.html] +tags = openwindow +[test_sibling_navigation_by_location.html] +tags = openwindow +[test_top_navigation_by_location_exotic.html] +[test_top_navigation_by_location.html] +[test_top_navigation_by_user_activation.html] +support-files = + file_top_navigation_by_user_activation.html + file_top_navigation_by_user_activation_iframe.html +skip-if = + http3 diff --git a/docshell/test/iframesandbox/test_child_navigation_by_location.html b/docshell/test/iframesandbox/test_child_navigation_by_location.html new file mode 100644 index 0000000000..383320a02b --- /dev/null +++ b/docshell/test/iframesandbox/test_child_navigation_by_location.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=785310 +html5 sandboxed iframe should not be able to perform top navigation with scripts allowed +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 785310 - iframe sandbox child navigation by location tests</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script> + SimpleTest.waitForExplicitFinish(); + + var testDataUri = "file_child_navigation_by_location.html"; + + function runScriptNavigationTest(testCase) { + window.onmessage = function(event) { + if (event.data != "childIframe") { + ok(false, "event.data: got '" + event.data + "', expected 'childIframe'"); + } + ok(!testCase.shouldBeBlocked, testCase.desc + " - child navigation was NOT blocked"); + runNextTest(); + }; + try { + window.parentIframe.eval(testCase.script); + } catch (e) { + ok(testCase.shouldBeBlocked, testCase.desc + " - " + e.message); + runNextTest(); + } + } + + var testCaseIndex = -1; + var testCases = [ + { + desc: "Test 1: cross origin child location.replace should NOT be blocked", + script: "window['crossOriginChildIframe'].location.replace(\"" + testDataUri + "\")", + shouldBeBlocked: false, + }, + { + desc: "Test 2: cross origin child location.assign should be blocked", + script: "window['crossOriginChildIframe'].location.assign(\"" + testDataUri + "\")", + shouldBeBlocked: true, + }, + { + desc: "Test 3: same origin child location.assign should NOT be blocked", + script: "window['sameOriginChildIframe'].location.assign(\"" + testDataUri + "\")", + shouldBeBlocked: false, + }, + { + desc: "Test 4: cross origin child location.href should NOT be blocked", + script: "window['crossOriginChildIframe'].location.href = \"" + testDataUri + "\"", + shouldBeBlocked: false, + }, + { + desc: "Test 5: cross origin child location.hash should be blocked", + script: "window['crossOriginChildIframe'].location.hash = 'wibble'", + shouldBeBlocked: true, + }, + { + desc: "Test 6: same origin child location.hash should NOT be blocked", + script: "window['sameOriginChildIframe'].location.hash = 'wibble'", + shouldBeBlocked: false, + }, + ]; + + function runNextTest() { + ++testCaseIndex; + if (testCaseIndex == testCases.length) { + SimpleTest.finish(); + return; + } + + runScriptNavigationTest(testCases[testCaseIndex]); + } + + addLoadEvent(runNextTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a> +<p id="display"></p> +<div id="content"> +Tests for Bug 785310 +</div> + +<iframe name="parentIframe" sandbox="allow-scripts allow-same-origin" srcdoc="<iframe name='sameOriginChildIframe'></iframe><iframe name='crossOriginChildIframe' sandbox='allow-scripts'></iframe>"</iframe> + +</body> +</html> diff --git a/docshell/test/iframesandbox/test_marquee_event_handlers.html b/docshell/test/iframesandbox/test_marquee_event_handlers.html new file mode 100644 index 0000000000..80added8ab --- /dev/null +++ b/docshell/test/iframesandbox/test_marquee_event_handlers.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1277475 +html5 sandboxed iframe should not run marquee attribute event handlers without allow-scripts +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 1277475 - html5 sandboxed iframe should not run marquee attribute event handlers without allow-scripts</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1277475">Mozilla Bug 1277475</a> +<p id="display"></p> +<div id="content">Tests for Bug 1277475</div> + +<iframe id="if1" name="if1" src="file_marquee_event_handlers.html" + sandbox="allow-same-origin allow-forms allow-top-navigation allow-pointer-lock allow-orientation-lock allow-popups allow-modals allow-popups-to-escape-sandbox"> +</iframe> + +<iframe id="if2" name="if2" src="file_marquee_event_handlers.html" + sandbox="allow-scripts"></iframe> + +<script> + SimpleTest.waitForExplicitFinish(); + + var expectedMessages = new Set(); + var numberOfMessagesExpected = 4; + var unexpectedMessages = new Set(); + + window.onmessage = function(event) { + info(event.data + " message received"); + if (event.data.startsWith("if2") || event.data == "if1 chaser") { + expectedMessages.add(event.data); + if (expectedMessages.size == numberOfMessagesExpected) { + checkRecievedMessages(); + } + } else { + unexpectedMessages.add(event.data); + } + }; + + function checkRecievedMessages() { + // Check the expected messages explicitly as a cross-check. + ok(expectedMessages.has("if1 chaser"), + "if1 chaser message should have been received"); + ok(expectedMessages.has("if2 marquee onstart"), + "if2 marquee onstart should have run in iframe sandbox with allow-scripts"); + ok(expectedMessages.has("if2 marquee onbounce"), + "if2 marquee onbounce should have run in iframe sandbox with allow-scripts"); + ok(expectedMessages.has("if2 marquee onfinish"), + "if2 marquee onfinish should have run in iframe sandbox with allow-scripts"); + + unexpectedMessages.forEach( + (v) => { + ok(false, v + " should NOT have run in iframe sandbox without allow-scripts"); + } + ); + + SimpleTest.finish(); + } + + // If things are working properly the attribute event handlers won't run on + // the marquee in if1, so add our own capturing listeners on its window, so we + // know when they have fired. (These will run as we are not sandboxed.) + var if1FiredEvents = new Set(); + var if1NumberOfEventsExpected = 3; + var if1Win = document.getElementById("if1").contentWindow; + if1Win.addEventListener("start", () => { checkMarqueeEvent("start"); }, true); + if1Win.addEventListener("bounce", () => { checkMarqueeEvent("bounce"); }, true); + if1Win.addEventListener("finish", () => { checkMarqueeEvent("finish"); }, true); + + function checkMarqueeEvent(eventType) { + info("if1 event " + eventType + " fired"); + if1FiredEvents.add(eventType); + if (if1FiredEvents.size == if1NumberOfEventsExpected) { + // Only send the chasing message after a tick of the event loop to allow + // event handlers on the marquee to process. + SimpleTest.executeSoon(sendChasingMessage); + } + } + + function sendChasingMessage() { + // Add our own message listener to if1's window and echo back a chasing + // message to make sure that any messages from incorrectly run marquee + // attribute event handlers should have arrived before it. + if1Win.addEventListener("message", + (e) => { if1Win.parent.postMessage(e.data, "*"); }); + if1Win.postMessage("if1 chaser", "*"); + info("if1 chaser message sent"); + } +</script> +</body> +</html> diff --git a/docshell/test/iframesandbox/test_other_auxiliary_navigation_by_location.html b/docshell/test/iframesandbox/test_other_auxiliary_navigation_by_location.html new file mode 100644 index 0000000000..3440878db7 --- /dev/null +++ b/docshell/test/iframesandbox/test_other_auxiliary_navigation_by_location.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=785310 +html5 sandboxed iframe should not be able to perform top navigation with scripts allowed +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 785310 - iframe sandbox other auxiliary navigation by location tests</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script> + SimpleTest.waitForExplicitFinish(); + + function runScriptNavigationTest(testCase) { + window.onmessage = function(event) { + if (event.data != "otherWindow") { + ok(false, "event.data: got '" + event.data + "', expected 'otherWindow'"); + } + ok(false, testCase.desc + " - auxiliary navigation was NOT blocked"); + runNextTest(); + }; + try { + window.testIframe.eval(testCase.script); + } catch (e) { + ok(true, testCase.desc + " - " + e.message); + runNextTest(); + } + } + + var testCaseIndex = -1; + var testCases = [ + { + desc: "Test 1: location.replace on auxiliary NOT opened by us should be blocked", + script: "parent.openedWindow.location.replace('file_other_auxiliary_navigation_by_location.html')", + }, + { + desc: "Test 2: location.assign on auxiliary NOT opened by us should be blocked", + script: "parent.openedWindow.location.assign('file_other_auxiliary_navigation_by_location.html')", + }, + { + desc: "Test 3: location.href on auxiliary NOT opened by us should be blocked", + script: "parent.openedWindow.location.href = 'file_other_auxiliary_navigation_by_location.html'", + }, + { + desc: "Test 4: location.hash on auxiliary NOT opened by us should be blocked", + script: "parent.openedWindow.location.hash = 'wibble'", + }, + ]; + + function runNextTest() { + ++testCaseIndex; + if (testCaseIndex == testCases.length) { + window.openedWindow.close(); + SimpleTest.finish(); + return; + } + + runScriptNavigationTest(testCases[testCaseIndex]); + } + + window.onmessage = runNextTest; + + window.onload = function() { + window.openedWindow = window.open("file_other_auxiliary_navigation_by_location.html", "otherWindow"); + }; +</script> + +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a> +<p id="display"></p> +<div id="content"> +Tests for Bug 785310 +</div> + +<iframe name="testIframe" sandbox="allow-scripts allow-same-origin allow-top-navigation allow-popups"></iframe> +</body> +</html> diff --git a/docshell/test/iframesandbox/test_our_auxiliary_navigation_by_location.html b/docshell/test/iframesandbox/test_our_auxiliary_navigation_by_location.html new file mode 100644 index 0000000000..1719f566a5 --- /dev/null +++ b/docshell/test/iframesandbox/test_our_auxiliary_navigation_by_location.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=785310 +html5 sandboxed iframe should not be able to perform top navigation with scripts allowed +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 785310 - iframe sandbox our auxiliary navigation by location tests</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script> + SimpleTest.waitForExplicitFinish(); + + function runScriptNavigationTest(testCase) { + window.onmessage = function(event) { + if (event.data != "ourWindow") { + ok(false, "event.data: got '" + event.data + "', expected 'ourWindow'"); + } + ok(!testCase.shouldBeBlocked, testCase.desc + " - auxiliary navigation was NOT blocked"); + runNextTest(); + }; + try { + SpecialPowers.wrap(window.testIframe).eval(testCase.script); + } catch (e) { + ok(testCase.shouldBeBlocked, testCase.desc + " - " + SpecialPowers.wrap(e).message); + runNextTest(); + } + } + + var testCaseIndex = -1; + var testCases = [ + { + desc: "Test 1: location.replace on auxiliary opened by us should NOT be blocked", + script: "openedWindow.location.replace('file_our_auxiliary_navigation_by_location.html')", + shouldBeBlocked: false, + }, + { + desc: "Test 2: location.assign on auxiliary opened by us should be blocked without allow-same-origin", + script: "openedWindow.location.assign('file_our_auxiliary_navigation_by_location.html')", + shouldBeBlocked: true, + }, + { + desc: "Test 3: location.href on auxiliary opened by us should NOT be blocked", + script: "openedWindow.location.href = 'file_our_auxiliary_navigation_by_location.html'", + shouldBeBlocked: false, + }, + { + desc: "Test 4: location.hash on auxiliary opened by us should be blocked without allow-same-origin", + script: "openedWindow.location.hash = 'wibble'", + shouldBeBlocked: true, + }, + ]; + + function runNextTest() { + ++testCaseIndex; + if (testCaseIndex == testCases.length) { + SpecialPowers.wrap(window.testIframe).eval("openedWindow.close()"); + SimpleTest.finish(); + return; + } + + runScriptNavigationTest(testCases[testCaseIndex]); + } + + window.onmessage = runNextTest; + + window.onload = function() { + SpecialPowers.wrap(window.testIframe).eval("var openedWindow = window.open('file_our_auxiliary_navigation_by_location.html', 'ourWindow')"); + }; +</script> + +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a> +<p id="display"></p> +<div id="content"> +Tests for Bug 785310 +</div> + +<iframe name="testIframe" sandbox="allow-scripts allow-popups"></iframe> +</body> +</html> diff --git a/docshell/test/iframesandbox/test_parent_navigation_by_location.html b/docshell/test/iframesandbox/test_parent_navigation_by_location.html new file mode 100644 index 0000000000..ac6977a3f3 --- /dev/null +++ b/docshell/test/iframesandbox/test_parent_navigation_by_location.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=785310 +html5 sandboxed iframe should not be able to perform top navigation with scripts allowed +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 785310 - iframe sandbox parent navigation by location tests</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script> + SimpleTest.waitForExplicitFinish(); + + function runScriptNavigationTest(testCase) { + window.onmessage = function(event) { + if (event.data != "parentIframe") { + ok(false, "event.data: got '" + event.data + "', expected 'parentIframe'"); + } + ok(false, testCase.desc + " - parent navigation was NOT blocked"); + runNextTest(); + }; + try { + window.parentIframe.childIframe.eval(testCase.script); + } catch (e) { + ok(true, testCase.desc + " - " + e.message); + runNextTest(); + } + } + + var testCaseIndex = -1; + var testCases = [ + { + desc: "Test 1: parent.location.replace should be blocked even when sandboxed with allow-same-origin allow-top-navigation", + script: "parent.location.replace('file_parent_navigation_by_location.html')", + }, + { + desc: "Test 2: parent.location.assign should be blocked even when sandboxed with allow-same-origin allow-top-navigation", + script: "parent.location.assign('file_parent_navigation_by_location.html')", + }, + { + desc: "Test 3: parent.location.href should be blocked even when sandboxed with allow-same-origin allow-top-navigation", + script: "parent.location.href = 'file_parent_navigation_by_location.html'", + }, + { + desc: "Test 4: parent.location.hash should be blocked even when sandboxed with allow-same-origin allow-top-navigation", + script: "parent.location.hash = 'wibble'", + }, + ]; + + function runNextTest() { + ++testCaseIndex; + if (testCaseIndex == testCases.length) { + SimpleTest.finish(); + return; + } + + runScriptNavigationTest(testCases[testCaseIndex]); + } + + window.onmessage = runNextTest; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a> +<p id="display"></p> +<div id="content"> +Tests for Bug 785310 +</div> + +<iframe name="parentIframe" src="file_parent_navigation_by_location.html"></iframe> + +</body> +</html> diff --git a/docshell/test/iframesandbox/test_sibling_navigation_by_location.html b/docshell/test/iframesandbox/test_sibling_navigation_by_location.html new file mode 100644 index 0000000000..d7508d5748 --- /dev/null +++ b/docshell/test/iframesandbox/test_sibling_navigation_by_location.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=785310 +html5 sandboxed iframe should not be able to perform top navigation with scripts allowed +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 785310 - iframe sandbox sibling navigation by location tests</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script> + SimpleTest.waitForExplicitFinish(); + + function runScriptNavigationTest(testCase) { + window.onmessage = function(event) { + if (event.data != "siblingIframe") { + ok(false, "event.data: got '" + event.data + "', expected 'siblingIframe'"); + } + + ok(false, testCase.desc + " - sibling navigation was NOT blocked"); + runNextTest(); + }; + + try { + window.testIframe.eval(testCase.script); + } catch (e) { + ok(true, testCase.desc + " - " + e.message); + runNextTest(); + } + } + + var testCaseIndex = -1; + var testCases = [ + { + desc: "Test 1: sibling location.replace should be blocked even when sandboxed with allow-same-origin allow-top-navigation", + script: "parent['siblingIframe'].location.replace('file_sibling_navigation_by_location.html')", + }, + { + desc: "Test 2: sibling location.assign should be blocked even when sandboxed with allow-same-origin allow-top-navigation", + script: "parent['siblingIframe'].location.assign('file_sibling_navigation_by_location.html')", + }, + { + desc: "Test 3: sibling location.href should be blocked even when sandboxed with allow-same-origin allow-top-navigation", + script: "parent['siblingIframe'].location.href = 'file_sibling_navigation_by_location.html'", + }, + { + desc: "Test 4: sibling location.hash should be blocked even when sandboxed with allow-same-origin allow-top-navigation", + script: "parent['siblingIframe'].location.hash = 'wibble'", + }, + ]; + + function runNextTest() { + ++testCaseIndex; + if (testCaseIndex == testCases.length) { + SimpleTest.finish(); + return; + } + + runScriptNavigationTest(testCases[testCaseIndex]); + } + + window.onmessage = runNextTest; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a> +<p id="display"></p> +<div id="content"> +Tests for Bug 785310 +</div> + +<iframe name="testIframe" sandbox="allow-scripts allow-same-origin allow-top-navigation"></iframe> +<iframe name="siblingIframe" src="file_sibling_navigation_by_location.html"></iframe> + +</body> +</html> diff --git a/docshell/test/iframesandbox/test_top_navigation_by_location.html b/docshell/test/iframesandbox/test_top_navigation_by_location.html new file mode 100644 index 0000000000..248f854bbf --- /dev/null +++ b/docshell/test/iframesandbox/test_top_navigation_by_location.html @@ -0,0 +1,167 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=785310 +html5 sandboxed iframe should not be able to perform top navigation with scripts allowed +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 785310 - iframe sandbox top navigation by location tests</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> + SimpleTest.waitForExplicitFinish(); + + var testWin; + + function runScriptNavigationTest(testCase) { + window.onmessage = function(event) { + if (event.data != "newTop") { + ok(false, "event.data: got '" + event.data + "', expected 'newTop'"); + } + ok(!testCase.shouldBeBlocked, testCase.desc + " - top navigation was NOT blocked"); + runNextTest(); + }; + try { + SpecialPowers.wrap(testWin[testCase.iframeName]).eval(testCase.script); + } catch (e) { + ok(testCase.shouldBeBlocked, testCase.desc + " - " + SpecialPowers.wrap(e).message); + runNextTest(); + } + } + + var testCaseIndex = -1; + var testCases = [ + { + desc: "Test 1: top.location.replace should be blocked when sandboxed without allow-top-navigation", + script: "top.location.replace('file_top_navigation_by_location.html')", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 2: top.location.assign should be blocked when sandboxed without allow-top-navigation", + script: "top.location.assign('file_top_navigation_by_location.html')", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 3: top.location.href should be blocked when sandboxed without allow-top-navigation", + script: "top.location.href = 'file_top_navigation_by_location.html'", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 4: top.location.pathname should be blocked when sandboxed without allow-top-navigation", + script: "top.location.pathname = top.location.pathname", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 5: top.location should be blocked when sandboxed without allow-top-navigation", + script: "top.location = 'file_top_navigation_by_location.html'", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 6: top.location.hash should be blocked when sandboxed without allow-top-navigation", + script: "top.location.hash = 'wibble'", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 7: top.location.replace should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation", + script: "top.location.replace('file_top_navigation_by_location.html')", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 8: top.location.assign should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation", + script: "top.location.assign('file_top_navigation_by_location.html')", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 9: top.location.href should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation", + script: "top.location.href = 'file_top_navigation_by_location.html'", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 10: top.location.pathname should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation", + script: "top.location.pathname = top.location.pathname", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 11: top.location should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation", + script: "top.location = 'file_top_navigation_by_location.html'", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 12: top.location.hash should NOT be blocked when sandboxed with allow-same-origin allow-top-navigation", + script: "top.location.hash = 'wibble'", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 13: top.location.replace should NOT be blocked when sandboxed with allow-top-navigation, but without allow-same-origin", + script: "top.location.replace('file_top_navigation_by_location.html')", + iframeName: "if3", + shouldBeBlocked: false, + }, + { + desc: "Test 14: top.location.assign should be blocked when sandboxed with allow-top-navigation, but without allow-same-origin", + script: "top.location.assign('file_top_navigation_by_location.html')", + iframeName: "if3", + shouldBeBlocked: true, + }, + { + desc: "Test 15: top.location.href should NOT be blocked when sandboxed with allow-top-navigation, but without allow-same-origin", + script: "top.location.href = 'file_top_navigation_by_location.html'", + iframeName: "if3", + shouldBeBlocked: false, + }, + { + desc: "Test 16: top.location.pathname should be blocked when sandboxed with allow-top-navigation, but without allow-same-origin", + script: "top.location.pathname = top.location.pathname", + iframeName: "if3", + shouldBeBlocked: true, + }, + { + desc: "Test 17: top.location should NOT be blocked when sandboxed with allow-top-navigation, but without allow-same-origin", + script: "top.location = 'file_top_navigation_by_location.html'", + iframeName: "if3", + shouldBeBlocked: false, + }, + { + desc: "Test 18: top.location.hash should be blocked when sandboxed with allow-top-navigation, but without allow-same-origin", + script: "top.location.hash = 'wibble'", + iframeName: "if3", + shouldBeBlocked: true, + }, + ]; + + function runNextTest() { + ++testCaseIndex; + if (testCaseIndex == testCases.length) { + testWin.close(); + SimpleTest.finish(); + return; + } + + runScriptNavigationTest(testCases[testCaseIndex]); + } + + window.onmessage = runNextTest; + testWin = window.open("file_top_navigation_by_location.html", "newTop"); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a> +<p id="display"></p> +<div id="content"> +Tests for Bug 785310 +</div> +</body> +</html> diff --git a/docshell/test/iframesandbox/test_top_navigation_by_location_exotic.html b/docshell/test/iframesandbox/test_top_navigation_by_location_exotic.html new file mode 100644 index 0000000000..11b7c78699 --- /dev/null +++ b/docshell/test/iframesandbox/test_top_navigation_by_location_exotic.html @@ -0,0 +1,204 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=785310 +html5 sandboxed iframe should not be able to perform top navigation with scripts allowed +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 785310 - iframe sandbox top navigation by location via exotic means tests</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> + SimpleTest.waitForExplicitFinish(); + + var testWin; + + function runScriptNavigationTest(testCase) { + window.onmessage = function(event) { + if (event.data.name != "newWindow") { + ok(false, "event.data.name: got '" + event.data.name + "', expected 'newWindow'"); + } + var diag = "top navigation was " + (event.data.blocked ? "" : "NOT ") + "blocked"; + ok((testCase.shouldBeBlocked == event.data.blocked), testCase.desc + " - " + diag); + runNextTest(); + }; + try { + testWin[testCase.iframeName].eval(testCase.script); + } catch (e) { + ok(testCase.shouldBeBlocked, testCase.desc + " - " + e.message); + runNextTest(); + } + } + + var testCaseIndex = -1; + var testCases = [ + { + desc: "Test 1: location.replace.call(top.location, ...) should be blocked when sandboxed without allow-top-navigation", + script: "location.replace.call(top.location, 'file_top_navigation_by_location_exotic.html')", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 2: location.replace.bind(top.location, ...) should be blocked when sandboxed without allow-top-navigation", + script: "location.replace.bind(top.location, 'file_top_navigation_by_location_exotic.html')()", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 3: Function.bind.call(location.replace, top.location, ...) should be blocked when sandboxed without allow-top-navigation", + script: "Function.bind.call(location.replace, top.location, 'file_top_navigation_by_location_exotic.html')()", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 4: location.replace.call(top.location, ...) should NOT be blocked when sandboxed with allow-top-navigation", + script: "location.replace.call(top.location, 'file_top_navigation_by_location_exotic.html')", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 5: location.replace.bind(top.location, ...) should NOT be blocked when sandboxed with allow-top-navigation", + script: "location.replace.bind(top.location, 'file_top_navigation_by_location_exotic.html')()", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 6: Function.bind.call(location.replace, top.location, ...) should NOT be blocked when sandboxed with allow-top-navigation", + script: "Function.bind.call(location.replace, top.location, 'file_top_navigation_by_location_exotic.html')()", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 7: top.location.href, via setTimeout, should be blocked when sandboxed without allow-top-navigation", + script: "setTimeout(function() { try { top.location.href = 'file_top_navigation_by_location_exotic.html' } catch (e) { top.onBlock() } }, 0)", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 8: top.location.href, via setTimeout, should NOT be blocked when sandboxed with allow-top-navigation", + script: "setTimeout(function() { try { top.location.href = 'file_top_navigation_by_location_exotic.html' } catch(e) { top.onBlock() } }, 0)", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 9: top.location.href, via eval, should be blocked when sandboxed without allow-top-navigation", + script: "eval('top.location.href = \"file_top_navigation_by_location_exotic.html\"')", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 10: top.location.href, via eval, should NOT be blocked when sandboxed with allow-top-navigation", + script: "eval('top.location.href = \"file_top_navigation_by_location_exotic.html\"')", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 11: top.location.href, via anonymous function, should be blocked when sandboxed without allow-top-navigation", + script: "(function() { top.location.href = 'file_top_navigation_by_location_exotic.html' })()", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 12: top.location.href, via anonymous function, should NOT be blocked when sandboxed with allow-top-navigation", + script: "(function() { top.location.href = 'file_top_navigation_by_location_exotic.html' })()", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 13: top.location.href, via function inserted in top, should be blocked when sandboxed without allow-top-navigation", + script: "top.doTest = function() { top.location.href = 'file_top_navigation_by_location_exotic.html' }; top.doTest();", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 14: top.location.href, via function inserted in top, should NOT be blocked when sandboxed with allow-top-navigation", + script: "top.doTest = function() { top.location.href = 'file_top_navigation_by_location_exotic.html' }; top.doTest();", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 15: top.location.href, via function inserted in us by top, should NOT be blocked when sandboxed without allow-top-navigation", + script: "top.eval('window[\"if1\"].doTest = function() { top.location.href = \"file_top_navigation_by_location_exotic.html\" };'), doTest();", + iframeName: "if1", + shouldBeBlocked: false, + }, + { + desc: "Test 16: top.location.href, via function inserted in top, should NOT be blocked when sandboxed with allow-top-navigation", + script: "top.eval('window[\"if2\"].doTest = function() { top.location.href = \"file_top_navigation_by_location_exotic.html\" };'), doTest();", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 17: top.location.href, via function in top, should NOT be blocked when sandboxed without allow-top-navigation", + script: "top.setOwnHref()", + iframeName: "if1", + shouldBeBlocked: false, + }, + { + desc: "Test 18: top.location.href, via function in top, should NOT be blocked when sandboxed with allow-top-navigation", + script: "top.setOwnHref()", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 19: top.location.href, via eval in top, should NOT be blocked when sandboxed without allow-top-navigation", + script: "top.eval('location.href = \"file_top_navigation_by_location_exotic.html\"')", + iframeName: "if1", + shouldBeBlocked: false, + }, + { + desc: "Test 20: top.location.href, via eval in top, should NOT be blocked when sandboxed with allow-top-navigation", + script: "top.eval('location.href = \"file_top_navigation_by_location_exotic.html\"')", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 21: top.location.href, via eval in top calling us, should be blocked when sandboxed without allow-top-navigation", + script: "function doTest() { top.location.href = 'file_top_navigation_by_location_exotic.html' } top.eval('window[\"if1\"].doTest()');", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 22: top.location.href, via eval in top calling us, should NOT be blocked when sandboxed with allow-top-navigation", + script: "function doTest() { top.location.href = 'file_top_navigation_by_location_exotic.html' } top.eval('window[\"if2\"].doTest()');", + iframeName: "if2", + shouldBeBlocked: false, + }, + { + desc: "Test 23: top.location.href, via function bound to top, should be blocked when sandboxed without allow-top-navigation", + script: "(function() { top.location.href = 'file_top_navigation_by_location_exotic.html' }).bind(top)();", + iframeName: "if1", + shouldBeBlocked: true, + }, + { + desc: "Test 24: top.location.href, via function bound to top, should NOT be blocked when sandboxed with allow-top-navigation", + script: "(function() { top.location.href = 'file_top_navigation_by_location_exotic.html' }).bind(top)();", + iframeName: "if2", + shouldBeBlocked: false, + }, + ]; + + function runNextTest() { + ++testCaseIndex; + if (testCaseIndex == testCases.length) { + testWin.close(); + SimpleTest.finish(); + return; + } + + runScriptNavigationTest(testCases[testCaseIndex]); + } + + window.onmessage = runNextTest; + testWin = window.open("file_top_navigation_by_location_exotic.html", "newWindow"); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=785310">Mozilla Bug 785310</a> +<p id="display"></p> +<div id="content"> +Tests for Bug 785310 +</div> +</body> +</html> diff --git a/docshell/test/iframesandbox/test_top_navigation_by_user_activation.html b/docshell/test/iframesandbox/test_top_navigation_by_user_activation.html new file mode 100644 index 0000000000..b462c54d7b --- /dev/null +++ b/docshell/test/iframesandbox/test_top_navigation_by_user_activation.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1744321 +--> +<head> +<meta charset="utf-8"> +<title>Iframe sandbox top navigation by user activation</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +function waitForMessage(aCallback) { + return new Promise((aResolve) => { + window.addEventListener("message", function listener(aEvent) { + aCallback(aEvent); + aResolve(); + }, { once: true }); + }); +} + +[ + { + desc: "A same-origin iframe in sandbox with 'allow-top-navigation-by-user-activation' cannot navigate its top level page, if the navigation is not triggered by a user gesture", + sameOrigin: true, + userGesture: false, + }, + { + desc: "A same-origin iframe in sandbox with 'allow-top-navigation-by-user-activation' can navigate its top level page, if the navigation is triggered by a user gesture", + sameOrigin: true, + userGesture: true, + }, + { + desc: "A cross-origin iframe in sandbox with 'allow-top-navigation-by-user-activation' cannot navigate its top level page, if the navigation is not triggered by a user gesture", + sameOrigin: false, + userGesture: false, + }, + { + desc: "A cross-origin iframe in sandbox with 'allow-top-navigation-by-user-activation' can navigate its top level page, if the navigation is triggered by a user gesture", + sameOrigin: false, + userGesture: true, + }, +].forEach(({desc, sameOrigin, userGesture}) => { + add_task(async function() { + info(`Test: ${desc}`); + + let url = "file_top_navigation_by_user_activation.html"; + if (sameOrigin) { + url = `${location.origin}/tests/docshell/test/iframesandbox/${url}`; + } + + let promise = waitForMessage((e) => { + is(e.data, "READY", "Ready for test"); + }); + let testWin = window.open(url); + await promise; + + promise = waitForMessage((e) => { + is(e.data, userGesture ? "NAVIGATED" : "BLOCKED", "Check the result"); + }); + testWin.postMessage(userGesture ? "CLICK" : "SCRIPT", "*"); + await promise; + testWin.close(); + }); +}); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1744321">Mozilla Bug 1744321</a> +<p id="display"></p> +<div id="content"> +Tests for Bug 1744321 +</div> +</body> +</html> diff --git a/docshell/test/mochitest/bug1422334_redirect.html b/docshell/test/mochitest/bug1422334_redirect.html new file mode 100644 index 0000000000..eec7fda2c7 --- /dev/null +++ b/docshell/test/mochitest/bug1422334_redirect.html @@ -0,0 +1,3 @@ +<html> + <body>You should never see this</body> +</html> diff --git a/docshell/test/mochitest/bug1422334_redirect.html^headers^ b/docshell/test/mochitest/bug1422334_redirect.html^headers^ new file mode 100644 index 0000000000..fbf2d1b745 --- /dev/null +++ b/docshell/test/mochitest/bug1422334_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Moved Temporarily +Location: ../navigation/blank.html?x=y diff --git a/docshell/test/mochitest/bug404548-subframe.html b/docshell/test/mochitest/bug404548-subframe.html new file mode 100644 index 0000000000..9a248b40b3 --- /dev/null +++ b/docshell/test/mochitest/bug404548-subframe.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<body onload="setTimeout(function() { window.location = 'bug404548-subframe_window.html'; }, 10)"> +<iframe srcdoc="<body onpagehide='var p = window.parent.opener; var e = window.frameElement; e.parentNode.removeChild(e); if (e.parentNode == null && e.contentWindow == null) { p.firstRemoved = true; }'>"> +</iframe> +<iframe srcdoc="<body onpagehide='window.parent.opener.secondHidden = true;'>"> +</iframe> diff --git a/docshell/test/mochitest/bug404548-subframe_window.html b/docshell/test/mochitest/bug404548-subframe_window.html new file mode 100644 index 0000000000..82ea73ea83 --- /dev/null +++ b/docshell/test/mochitest/bug404548-subframe_window.html @@ -0,0 +1 @@ +<body onload='window.opener.finishTest()'> diff --git a/docshell/test/mochitest/bug413310-post.sjs b/docshell/test/mochitest/bug413310-post.sjs new file mode 100644 index 0000000000..f87937ab56 --- /dev/null +++ b/docshell/test/mochitest/bug413310-post.sjs @@ -0,0 +1,10 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html"); + response.write( + "<body onload='window.parent.onloadCount++'>" + + request.method + + " " + + Date.now() + + "</body>" + ); +} diff --git a/docshell/test/mochitest/bug413310-subframe.html b/docshell/test/mochitest/bug413310-subframe.html new file mode 100644 index 0000000000..bcff1886fd --- /dev/null +++ b/docshell/test/mochitest/bug413310-subframe.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <body onload="window.parent.onloadCount++"> + <form action="bug413310-post.sjs" method="POST"> + </form> + </body> +</html> diff --git a/docshell/test/mochitest/bug529119-window.html b/docshell/test/mochitest/bug529119-window.html new file mode 100644 index 0000000000..f1908835a7 --- /dev/null +++ b/docshell/test/mochitest/bug529119-window.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test bug 529119, sub-window</title> +<body onload="window.opener.windowLoaded();"> +</body> +</html> diff --git a/docshell/test/mochitest/bug530396-noref.sjs b/docshell/test/mochitest/bug530396-noref.sjs new file mode 100644 index 0000000000..6a65882160 --- /dev/null +++ b/docshell/test/mochitest/bug530396-noref.sjs @@ -0,0 +1,22 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html"); + response.setHeader("Cache-Control", "no-cache"); + response.write("<body onload='"); + + if (!request.hasHeader("Referer")) { + response.write("window.parent.onloadCount++;"); + } + + if (request.queryString == "newwindow") { + response.write( + "if (window.opener) { window.opener.parent.onloadCount++; window.opener.parent.doNextStep(); }" + ); + response.write("if (!window.opener) window.close();"); + response.write("'>"); + } else { + response.write("window.parent.doNextStep();'>"); + } + + response.write(request.method + " " + Date.now()); + response.write("</body>"); +} diff --git a/docshell/test/mochitest/bug530396-subframe.html b/docshell/test/mochitest/bug530396-subframe.html new file mode 100644 index 0000000000..be81b9f144 --- /dev/null +++ b/docshell/test/mochitest/bug530396-subframe.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <body onload="window.parent.onloadCount++"> + <a href="bug530396-noref.sjs" rel="noreferrer foo" id="target1">bug530396-noref.sjs</a> + <a href="bug530396-noref.sjs?newwindow" rel="nofollow noreferrer" id="target2" target="newwindow">bug530396-noref.sjs with new window</a> + </body> +</html> diff --git a/docshell/test/mochitest/bug570341_recordevents.html b/docshell/test/mochitest/bug570341_recordevents.html new file mode 100644 index 0000000000..45b04866ec --- /dev/null +++ b/docshell/test/mochitest/bug570341_recordevents.html @@ -0,0 +1,21 @@ +<html> +<head> +<script> + var start = Date.now(); + window._testing_js_start = Date.now(); + window["_testing_js_after_" + document.readyState] = start; + document.addEventListener("DOMContentLoaded", + function() { + window._testing_evt_DOMContentLoaded = Date.now(); + }, true); + document.addEventListener("readystatechange", function() { + window["_testing_evt_DOM_" + document.readyState] = Date.now(); + }, true); + function recordLoad() { + window._testing_evt_load = Date.now(); + } +</script> +</head> +<body onload="recordLoad()">This document collects time +for events related to the page load progress.</body> +</html> diff --git a/docshell/test/mochitest/bug668513_redirect.html b/docshell/test/mochitest/bug668513_redirect.html new file mode 100644 index 0000000000..1b8f66c631 --- /dev/null +++ b/docshell/test/mochitest/bug668513_redirect.html @@ -0,0 +1 @@ +<html><body>This document is redirected to a blank document.</body></html> diff --git a/docshell/test/mochitest/bug668513_redirect.html^headers^ b/docshell/test/mochitest/bug668513_redirect.html^headers^ new file mode 100644 index 0000000000..0e785833c6 --- /dev/null +++ b/docshell/test/mochitest/bug668513_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Moved Temporarily +Location: navigation/blank.html diff --git a/docshell/test/mochitest/bug691547_frame.html b/docshell/test/mochitest/bug691547_frame.html new file mode 100644 index 0000000000..00172f7119 --- /dev/null +++ b/docshell/test/mochitest/bug691547_frame.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=691547 +--> +<head> + <title>Test for Bug 691547</title> +</head> +<body> +<iframe style="width:95%"></iframe> +</body> +</html> diff --git a/docshell/test/mochitest/clicker.html b/docshell/test/mochitest/clicker.html new file mode 100644 index 0000000000..b655e27ea5 --- /dev/null +++ b/docshell/test/mochitest/clicker.html @@ -0,0 +1,7 @@ +<!doctype html> +<script> + "use strict"; + let target = window.opener ? window.opener : window.parent; + + onmessage = ({data}) => target.postMessage({}, "*"); +</script> diff --git a/docshell/test/mochitest/double_submit.sjs b/docshell/test/mochitest/double_submit.sjs new file mode 100644 index 0000000000..487150d456 --- /dev/null +++ b/docshell/test/mochitest/double_submit.sjs @@ -0,0 +1,79 @@ +"use strict"; + +let self = this; + +let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function log(str) { + // dump(`LOG: ${str}\n`); +} + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + + return result.join(""); +} + +function now() { + return Date.now(); +} + +async function handleRequest(request, response) { + log("Get query parameters"); + Cu.importGlobalProperties(["URLSearchParams"]); + let params = new URLSearchParams(request.queryString); + + let start = now(); + let delay = parseInt(params.get("delay")) || 0; + log(`Delay for ${delay}`); + + let message = "good"; + if (request.method !== "POST") { + message = "bad"; + } else { + log("Read POST body"); + let body = new URLSearchParams( + readStream(new BinaryInputStream(request.bodyInputStream)) + ); + message = body.get("token") || "bad"; + log(`The result was ${message}`); + } + + let body = `<!doctype html> + <script> + "use strict"; + let target = (opener || parent); + target.postMessage(${JSON.stringify(message)}, '*'); + </script>`; + + // Sieze power from the response to allow manually transmitting data at any + // rate we want, so we can delay transmitting headers. + response.seizePower(); + + log(`Writing HTTP status line at ${now() - start}`); + response.write("HTTP/1.1 200 OK\r\n"); + + await new Promise(resolve => setTimeout(() => resolve(), delay)); + + log(`Delay completed at ${now() - start}`); + response.write("Content-Type: text/html\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + response.finish(); + + log("Finished"); +} diff --git a/docshell/test/mochitest/dummy_page.html b/docshell/test/mochitest/dummy_page.html new file mode 100644 index 0000000000..59bf2a5f8f --- /dev/null +++ b/docshell/test/mochitest/dummy_page.html @@ -0,0 +1,6 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + just a dummy html file + </body> +</html> diff --git a/docshell/test/mochitest/file_anchor_scroll_after_document_open.html b/docshell/test/mochitest/file_anchor_scroll_after_document_open.html new file mode 100644 index 0000000000..7903380eac --- /dev/null +++ b/docshell/test/mochitest/file_anchor_scroll_after_document_open.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<script> + if (location.hash == "#target") { + parent.postMessage("haveHash", "*"); + } else { + document.addEventListener("DOMContentLoaded", function() { + document.open(); + document.write("<!DOCTYPE html><html style='height: 100%'><body style='height: 100%'><div style='height: 200%'></div><div id='target'></div></body></html>"); + document.close(); + // Notify parent via postMessage, since otherwise exceptions will not get + // caught by its onerror handler. + parent.postMessage("doTest", "*"); + }); + } +</script> diff --git a/docshell/test/mochitest/file_bfcache_plus_hash_1.html b/docshell/test/mochitest/file_bfcache_plus_hash_1.html new file mode 100644 index 0000000000..199f6003e0 --- /dev/null +++ b/docshell/test/mochitest/file_bfcache_plus_hash_1.html @@ -0,0 +1,24 @@ +<html><body> + Popup 1 + <script type="application/javascript"> + var bc = new BroadcastChannel("bug646641_1"); + window.onload = () => { + bc.postMessage({ message: "childLoad", num: 1 }) + } + + window.onpageshow = () => { + bc.postMessage({ message: "childPageshow", num: 1 }) + } + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + if (msg == "pushState") { + history.pushState("", "", ""); + location = "file_bfcache_plus_hash_2.html"; + } else if (msg == "close") { + bc.postMessage({ message: "closed" }); + bc.close(); + window.close(); + } + } + </script> +</body></html> diff --git a/docshell/test/mochitest/file_bfcache_plus_hash_2.html b/docshell/test/mochitest/file_bfcache_plus_hash_2.html new file mode 100644 index 0000000000..c27d4eaa3b --- /dev/null +++ b/docshell/test/mochitest/file_bfcache_plus_hash_2.html @@ -0,0 +1,17 @@ +<html><body> + Popup 2 + <script type="application/javascript"> + var bc = new BroadcastChannel("bug646641_2"); + window.onload = () => { + bc.postMessage({ message: "childLoad", num: 2 }) + requestAnimationFrame(() => bc.postMessage({message: "childPageshow", num: 2})); + } + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + if (msg == "go-2") { + history.go(-2); + bc.close(); + } + } + </script> +</body></html> diff --git a/docshell/test/mochitest/file_bug1121701_1.html b/docshell/test/mochitest/file_bug1121701_1.html new file mode 100644 index 0000000000..62c58495f7 --- /dev/null +++ b/docshell/test/mochitest/file_bug1121701_1.html @@ -0,0 +1,29 @@ +<script> + var bc = new BroadcastChannel("file_bug1121701_1"); + var pageHideAsserts = undefined; + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "setInnerHTML") { + document.body.innerHTML = "modified"; + window.onpagehide = function(event) { + window.onpagehide = null; + pageHideAsserts = {}; + pageHideAsserts.persisted = event.persisted; + // eslint-disable-next-line no-unsanitized/property + pageHideAsserts.innerHTML = window.document.body.innerHTML; + }; + window.location.href = msg.testUrl2; + } else if (command == "close") { + bc.postMessage({command: "closed"}); + bc.close(); + window.close(); + } + } + window.onpageshow = function(e) { + var msg = {command: "child1PageShow", persisted: e.persisted, pageHideAsserts}; + // eslint-disable-next-line no-unsanitized/property + msg.innerHTML = window.document.body.innerHTML; + bc.postMessage(msg); + }; +</script> diff --git a/docshell/test/mochitest/file_bug1121701_2.html b/docshell/test/mochitest/file_bug1121701_2.html new file mode 100644 index 0000000000..6cec88cd5d --- /dev/null +++ b/docshell/test/mochitest/file_bug1121701_2.html @@ -0,0 +1,23 @@ +<script> + var bc = new BroadcastChannel("file_bug1121701_2"); + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "setInnerHTML") { + window.document.body.innerHTML = "<img>"; + window.onmessage = function() { + bc.postMessage({command: "onmessage"}); + window.document.body.firstChild.src = msg.location; + bc.close(); + }; + window.onbeforeunload = function() { + window.postMessage("foo", "*"); + }; + + history.back(); + } + } + window.onpageshow = function(e) { + bc.postMessage({command: "child2PageShow", persisted: e.persisted}); + }; +</script> diff --git a/docshell/test/mochitest/file_bug1151421.html b/docshell/test/mochitest/file_bug1151421.html new file mode 100644 index 0000000000..7bb8c8f363 --- /dev/null +++ b/docshell/test/mochitest/file_bug1151421.html @@ -0,0 +1,19 @@ +<html> +<head> +<style> +body, html { + height: 100%; +} +.spacer { + height: 80%; +} +</style> +</head> +<body onload='(parent || opener).childLoad()'> + +<div class="spacer"></div> +<div id="content">content</div> +<div class="spacer"></div> + +</body> +</html> diff --git a/docshell/test/mochitest/file_bug1186774.html b/docshell/test/mochitest/file_bug1186774.html new file mode 100644 index 0000000000..9af95b09bd --- /dev/null +++ b/docshell/test/mochitest/file_bug1186774.html @@ -0,0 +1 @@ +<div style='height: 9000px;'></div> diff --git a/docshell/test/mochitest/file_bug1450164.html b/docshell/test/mochitest/file_bug1450164.html new file mode 100644 index 0000000000..55e32ce93d --- /dev/null +++ b/docshell/test/mochitest/file_bug1450164.html @@ -0,0 +1,16 @@ +<html> + <head> + <script> + function go() { + var a = window.history.state; + window.history.replaceState(a, "", "1"); + var ok = opener.ok; + var SimpleTest = opener.SimpleTest; + ok("Addition of history in unload did not crash browser"); + SimpleTest.finish(); + } + </script> + </head> + <body onunload="go()"> + </body> +</html> diff --git a/docshell/test/mochitest/file_bug1729662.html b/docshell/test/mochitest/file_bug1729662.html new file mode 100644 index 0000000000..f5710e1c49 --- /dev/null +++ b/docshell/test/mochitest/file_bug1729662.html @@ -0,0 +1,8 @@ +<script> +addEventListener("load", () => { + (new BroadcastChannel("bug1729662")).postMessage("load"); + history.pushState(1, null, location.href); + history.back(); + history.forward(); +}); +</script> diff --git a/docshell/test/mochitest/file_bug1740516_1.html b/docshell/test/mochitest/file_bug1740516_1.html new file mode 100644 index 0000000000..ac8ca71d93 --- /dev/null +++ b/docshell/test/mochitest/file_bug1740516_1.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script> + window.addEventListener("pageshow", ({ persisted }) => { + let bc = new BroadcastChannel("bug1740516_1"); + bc.addEventListener("message", ({ data }) => { + bc.close(); + switch (data) { + case "block_bfcache_and_navigate": + window.blockBFCache = new RTCPeerConnection(); + // Fall through + case "navigate": + document.location = "file_bug1740516_2.html"; + break; + case "close": + window.close(); + break; + } + }); + bc.postMessage(persisted); + }); + </script> +</head> +<body> + <iframe src="file_bug1740516_1_inner.html"></iframe> +</body> +</html> diff --git a/docshell/test/mochitest/file_bug1740516_1_inner.html b/docshell/test/mochitest/file_bug1740516_1_inner.html new file mode 100644 index 0000000000..159c6bde5a --- /dev/null +++ b/docshell/test/mochitest/file_bug1740516_1_inner.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script> + window.addEventListener("pageshow", ({ persisted }) => { + let bc = new BroadcastChannel("bug1740516_1_inner"); + bc.postMessage(persisted); + bc.close(); + }); + </script> +</head> +<body> +</body> +</html> diff --git a/docshell/test/mochitest/file_bug1740516_2.html b/docshell/test/mochitest/file_bug1740516_2.html new file mode 100644 index 0000000000..2dc714feef --- /dev/null +++ b/docshell/test/mochitest/file_bug1740516_2.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script> + addEventListener("pageshow", () => { history.back(); }); + </script> +</head> +<body> +</body> +</html> diff --git a/docshell/test/mochitest/file_bug1741132.html b/docshell/test/mochitest/file_bug1741132.html new file mode 100644 index 0000000000..d863b9f015 --- /dev/null +++ b/docshell/test/mochitest/file_bug1741132.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script> + window.addEventListener("pageshow", ({ persisted }) => { + let bc = new BroadcastChannel("bug1741132"); + bc.addEventListener("message", ({ data: { cmd, arg } }) => { + bc.close(); + switch (cmd) { + case "load": + document.location = arg; + break; + case "go": + window.blockBFCache = new RTCPeerConnection(); + history.go(arg); + break; + case "close": + window.close(); + break; + } + }); + bc.postMessage(persisted); + }); + </script> +</head> +<body> +</body> +</html> diff --git a/docshell/test/mochitest/file_bug1742865.sjs b/docshell/test/mochitest/file_bug1742865.sjs new file mode 100644 index 0000000000..950c30ecd2 --- /dev/null +++ b/docshell/test/mochitest/file_bug1742865.sjs @@ -0,0 +1,77 @@ +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + if (request.queryString == "reset") { + setState("index", "0"); + response.setStatusLine(request.httpVersion, 200, "Ok"); + response.write("Reset"); + return; + } + + let refresh = ""; + let index = Number(getState("index")); + // index == 0 First load, returns first meta refresh + // index == 1 Second load, caused by first meta refresh, returns second meta refresh + // index == 2 Third load, caused by second meta refresh, doesn't return a meta refresh + let query = new URLSearchParams(request.queryString); + if (index < 2) { + refresh = query.get("seconds"); + if (query.get("crossOrigin") == "true") { + const hosts = ["example.org", "example.com"]; + + let url = `${request.scheme}://${hosts[index]}${request.path}?${request.queryString}`; + refresh += `; url=${url}`; + } + refresh = `<meta http-equiv="Refresh" content="${refresh}">`; + } + // We want to scroll for the first load, and check that the meta refreshes keep the same + // scroll position. + let scroll = index == 0 ? `scrollTo(0, ${query.get("scrollTo")});` : ""; + + setState("index", String(index + 1)); + + response.write( + `<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Cache-Control" content="no-cache"> + ${refresh} + <script> + window.addEventListener("pageshow", () => { + ${scroll} + window.top.opener.postMessage({ + commandType: "pageShow", + commandData: { + inputValue: document.getElementById("input").value, + scrollPosition: window.scrollY, + }, + }, "*"); + }); + window.addEventListener("message", ({ data }) => { + if (data == "changeInputValue") { + document.getElementById("input").value = "1234"; + window.top.opener.postMessage({ + commandType: "onChangedInputValue", + commandData: { + historyLength: history.length, + inputValue: document.getElementById("input").value, + }, + }, "*"); + } else if (data == "loadNext") { + location.href += "&loadnext=1"; + } else if (data == "back") { + history.back(); + } + }); + </script> +</head> +<body> +<input type="text" id="input" value="initial"></input> +<div style='height: 9000px;'></div> +<p> +</p> +</body> +</html>` + ); +} diff --git a/docshell/test/mochitest/file_bug1742865_outer.sjs b/docshell/test/mochitest/file_bug1742865_outer.sjs new file mode 100644 index 0000000000..bad8b23f00 --- /dev/null +++ b/docshell/test/mochitest/file_bug1742865_outer.sjs @@ -0,0 +1,25 @@ +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + response.write( + `<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <script> + window.addEventListener("message", ({ data }) => { + if (data == "loadNext") { + location.href += "&loadnext=1"; + return; + } + // Forward other messages to the frame. + document.getElementById("frame").contentWindow.postMessage(data, "*"); + }); + </script> +</head> +<body> + <iframe src="file_bug1742865.sjs?${request.queryString}" id="frame"></iframe> +</body> +</html>` + ); +} diff --git a/docshell/test/mochitest/file_bug1743353.html b/docshell/test/mochitest/file_bug1743353.html new file mode 100644 index 0000000000..c08f8d143f --- /dev/null +++ b/docshell/test/mochitest/file_bug1743353.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script> + window.addEventListener("pageshow", () => { + let bc = new BroadcastChannel("bug1743353"); + bc.addEventListener("message", ({ data: cmd }) => { + switch (cmd) { + case "load": + bc.close(); + document.location += "?1"; + break; + case "back": + window.blockBFCache = new RTCPeerConnection(); + window.addEventListener("pagehide", () => { + bc.postMessage("pagehide"); + }); + window.addEventListener("unload", () => { + bc.postMessage("unload"); + bc.close(); + }); + history.back(); + break; + case "close": + bc.close(); + window.close(); + break; + } + }); + bc.postMessage("pageshow"); + }); + </script> +</head> +<body> +</body> +</html> diff --git a/docshell/test/mochitest/file_bug1747033.sjs b/docshell/test/mochitest/file_bug1747033.sjs new file mode 100644 index 0000000000..14401101b2 --- /dev/null +++ b/docshell/test/mochitest/file_bug1747033.sjs @@ -0,0 +1,110 @@ +"use strict"; + +const BOUNDARY = "BOUNDARY"; + +// waitForPageShow should be false if this is for multipart/x-mixed-replace +// and it's not the last part, Gecko doesn't fire pageshow for those parts. +function documentString(waitForPageShow = true) { + return `<html> + <head> + <script> + let bc = new BroadcastChannel("bug1747033"); + bc.addEventListener("message", ({ data: { cmd, arg = undefined } }) => { + switch (cmd) { + case "load": + location.href = arg; + break; + case "replaceState": + history.replaceState({}, "Replaced state", arg); + bc.postMessage({ "historyLength": history.length, "location": location.href }); + break; + case "back": + history.back(); + break; + case "close": + close(); + break; + } + }); + + function reply() { + bc.postMessage({ "historyLength": history.length, "location": location.href }); + } + + ${waitForPageShow ? `addEventListener("pageshow", reply);` : "reply();"} + </script> + </head> + <body></body> +</html> +`; +} + +function boundary(last = false) { + let b = `--${BOUNDARY}`; + if (last) { + b += "--"; + } + return b + "\n"; +} + +function sendMultipart(response, last = false) { + setState("sendMore", ""); + + response.write(`Content-Type: text/html + +${documentString(last)} +`); + response.write(boundary(last)); +} + +function shouldSendMore() { + return new Promise(resolve => { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + let sendMore = getState("sendMore"); + if (sendMore !== "") { + timer.cancel(); + resolve(sendMore); + } + }, + 100, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + }); +} + +async function handleRequest(request, response) { + if (request.queryString == "") { + // This is for non-multipart/x-mixed-replace loads. + response.write(documentString()); + return; + } + + if (request.queryString == "sendNextPart") { + setState("sendMore", "next"); + return; + } + + if (request.queryString == "sendLastPart") { + setState("sendMore", "last"); + return; + } + + response.processAsync(); + + response.setHeader( + "Content-Type", + `multipart/x-mixed-replace; boundary=${BOUNDARY}`, + false + ); + response.setStatusLine(request.httpVersion, 200, "OK"); + + response.write(boundary()); + sendMultipart(response); + while ((await shouldSendMore("sendMore")) !== "last") { + sendMultipart(response); + } + sendMultipart(response, true); + response.finish(); +} diff --git a/docshell/test/mochitest/file_bug1773192_1.html b/docshell/test/mochitest/file_bug1773192_1.html new file mode 100644 index 0000000000..42d5b3fced --- /dev/null +++ b/docshell/test/mochitest/file_bug1773192_1.html @@ -0,0 +1,13 @@ +<script> + let bc = new BroadcastChannel("bug1743353"); + bc.addEventListener("message", ({ data }) => { + if (data == "next") { + location = "file_bug1773192_2.html"; + } else if (data == "close") { + window.close(); + } + }); + window.addEventListener("pageshow", () => { + bc.postMessage({ location: location.href, referrer: document.referrer }); + }); +</script> diff --git a/docshell/test/mochitest/file_bug1773192_2.html b/docshell/test/mochitest/file_bug1773192_2.html new file mode 100644 index 0000000000..3b1e5bcb28 --- /dev/null +++ b/docshell/test/mochitest/file_bug1773192_2.html @@ -0,0 +1,13 @@ +<html> + <head> + <meta http-equiv="Cache-Control" content="no-cache, no-store"> + <meta name="referrer" content="same-origin"> + </head> + <body> + <form method="POST" action="file_bug1773192_3.sjs"></form> + <script> + history.replaceState({}, "", document.referrer); + document.forms[0].submit(); + </script> + </body> +</html> diff --git a/docshell/test/mochitest/file_bug1773192_3.sjs b/docshell/test/mochitest/file_bug1773192_3.sjs new file mode 100644 index 0000000000..ce889c7035 --- /dev/null +++ b/docshell/test/mochitest/file_bug1773192_3.sjs @@ -0,0 +1,3 @@ +function handleRequest(request, response) { + response.write("<script>history.back();</script>"); +} diff --git a/docshell/test/mochitest/file_bug385434_1.html b/docshell/test/mochitest/file_bug385434_1.html new file mode 100644 index 0000000000..5c951f1fa6 --- /dev/null +++ b/docshell/test/mochitest/file_bug385434_1.html @@ -0,0 +1,29 @@ +<!-- +Inner frame for test of bug 385434. +https://bugzilla.mozilla.org/show_bug.cgi?id=385434 +--> +<html> +<head> + <script type="application/javascript"> + function hashchange() { + parent.onIframeHashchange(); + } + + function load() { + parent.onIframeLoad(); + } + + function scroll() { + parent.onIframeScroll(); + } + </script> +</head> + +<body onscroll="scroll()" onload="load()" onhashchange="hashchange()"> +<a href="#link1" id="link1">link1</a> +<!-- Our parent loads us in an iframe with height 100px, so this spacer ensures + that switching between #link1 and #link2 causes us to scroll --> +<div style="height:200px;"></div> +<a href="#link2" id="link2">link2</a> +</body> +</html> diff --git a/docshell/test/mochitest/file_bug385434_2.html b/docshell/test/mochitest/file_bug385434_2.html new file mode 100644 index 0000000000..4aa5ef82b8 --- /dev/null +++ b/docshell/test/mochitest/file_bug385434_2.html @@ -0,0 +1,26 @@ +<!-- +Inner frame for test of bug 385434. +https://bugzilla.mozilla.org/show_bug.cgi?id=385434 +--> +<html> +<head> + <script type="application/javascript"> + function hashchange(e) { + // pass the event back to the parent so it can check its properties. + parent.gSampleEvent = e; + + parent.statusMsg("Hashchange in 2."); + parent.onIframeHashchange(); + } + + function load() { + parent.statusMsg("Loading 2."); + parent.onIframeLoad(); + } + </script> +</head> + +<frameset onload="load()" onhashchange="hashchange(event)"> + <frame src="about:blank" /> +</frameset> +</html> diff --git a/docshell/test/mochitest/file_bug385434_3.html b/docshell/test/mochitest/file_bug385434_3.html new file mode 100644 index 0000000000..34dd68ef45 --- /dev/null +++ b/docshell/test/mochitest/file_bug385434_3.html @@ -0,0 +1,22 @@ +<!-- +Inner frame for test of bug 385434. +https://bugzilla.mozilla.org/show_bug.cgi?id=385434 +--> +<html> +<head> + <script type="application/javascript"> + // Notify our parent if we have a hashchange and once we're done loading. + window.addEventListener("hashchange", parent.onIframeHashchange); + + window.addEventListener("DOMContentLoaded", function() { + // This also should trigger a hashchange, becuase the readystate is + // "interactive", not "complete" during DOMContentLoaded. + window.location.hash = "2"; + }); + + </script> +</head> + +<body> +</body> +</html> diff --git a/docshell/test/mochitest/file_bug475636.sjs b/docshell/test/mochitest/file_bug475636.sjs new file mode 100644 index 0000000000..cba9a968d6 --- /dev/null +++ b/docshell/test/mochitest/file_bug475636.sjs @@ -0,0 +1,97 @@ +let jsURL = + "javascript:" + + escape( + 'window.parent.postMessage("JS uri ran", "*");\ +return \'\ +<script>\ +window.parent.postMessage("Able to access private: " +\ + window.parent.private, "*");\ +</script>\'' + ); +let dataURL = + "data:text/html," + + escape( + '<!DOCTYPE HTML>\ +<script>\ +try {\ + window.parent.postMessage("Able to access private: " +\ + window.parent.private, "*");\ +}\ +catch (e) {\ + window.parent.postMessage("pass", "*");\ +}\ +</script>' + ); + +let tests = [ + // Plain document should work as normal + '<!DOCTYPE HTML>\ +<script>\ +try {\ + window.parent.private;\ + window.parent.postMessage("pass", "*");\ +}\ +catch (e) {\ + window.parent.postMessage("Unble to access private", "*");\ +}\ +</script>', + + // refresh to plain doc + { refresh: "file_bug475636.sjs?1", doc: "<!DOCTYPE HTML>" }, + + // meta-refresh to plain doc + '<!DOCTYPE HTML>\ +<head>\ + <meta http-equiv="refresh" content="0; url=file_bug475636.sjs?1">\ +</head>', + + // refresh to data url + { refresh: dataURL, doc: "<!DOCTYPE HTML>" }, + + // meta-refresh to data url + '<!DOCTYPE HTML>\ +<head>\ + <meta http-equiv="refresh" content="0; url=' + + dataURL + + '">\ +</head>', + + // refresh to js url should not be followed + { + refresh: jsURL, + doc: '<!DOCTYPE HTML>\ +<script>\ +setTimeout(function() {\ + window.parent.postMessage("pass", "*");\ +}, 2000);\ +</script>', + }, + + // meta refresh to js url should not be followed + '<!DOCTYPE HTML>\ +<head>\ + <meta http-equiv="refresh" content="0; url=' + + jsURL + + '">\ +</head>\ +<script>\ +setTimeout(function() {\ + window.parent.postMessage("pass", "*");\ +}, 2000);\ +</script>', +]; + +function handleRequest(request, response) { + dump("@@@@@@@@@hi there: " + request.queryString + "\n"); + let test = tests[parseInt(request.queryString, 10) - 1]; + response.setHeader("Content-Type", "text/html"); + + if (!test) { + response.write('<script>parent.postMessage("done", "*");</script>'); + } else if (typeof test == "string") { + response.write(test); + } else if (test.refresh) { + response.setHeader("Refresh", "0; url=" + test.refresh); + response.write(test.doc); + } +} diff --git a/docshell/test/mochitest/file_bug509055.html b/docshell/test/mochitest/file_bug509055.html new file mode 100644 index 0000000000..ac30876bbf --- /dev/null +++ b/docshell/test/mochitest/file_bug509055.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test inner frame for bug 509055</title> +</head> +<body onhashchange="hashchangeCallback()"> + file_bug509055.html +</body> +</html> diff --git a/docshell/test/mochitest/file_bug511449.html b/docshell/test/mochitest/file_bug511449.html new file mode 100644 index 0000000000..637732dbbf --- /dev/null +++ b/docshell/test/mochitest/file_bug511449.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<title>Used in test for bug 511449</title> +<input type="text" id="input"> +<script type="text/javascript"> + document.getElementById("input").focus(); +</script> diff --git a/docshell/test/mochitest/file_bug540462.html b/docshell/test/mochitest/file_bug540462.html new file mode 100644 index 0000000000..ab8c07eba5 --- /dev/null +++ b/docshell/test/mochitest/file_bug540462.html @@ -0,0 +1,25 @@ +<html> + <head> + <script> + // <!-- + function test() { + document.open(); + document.write( + `<html> + <body onload='opener.documentWriteLoad(); rel();'> + <a href='foo.html'>foo</a> + <script> + function rel() { setTimeout('location.reload()', 0); } + <\/script> + </body> + </html>` + ); + document.close(); + } + // --> + </script> + </head> + <body onload="setTimeout('test()', 0)"> + Test for bug 540462 + </body> +</html> diff --git a/docshell/test/mochitest/file_bug580069_1.html b/docshell/test/mochitest/file_bug580069_1.html new file mode 100644 index 0000000000..7ab4610334 --- /dev/null +++ b/docshell/test/mochitest/file_bug580069_1.html @@ -0,0 +1,8 @@ +<html> +<body onload='parent.page1Load();'> +file_bug580069_1.html + +<form id='form' action='file_bug580069_2.sjs' method='POST'></form> + +</body> +</html> diff --git a/docshell/test/mochitest/file_bug580069_2.sjs b/docshell/test/mochitest/file_bug580069_2.sjs new file mode 100644 index 0000000000..ff03c74e68 --- /dev/null +++ b/docshell/test/mochitest/file_bug580069_2.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.write( + "<html><body onload='parent.page2Load(\"" + + request.method + + "\")'>file_bug580069_2.sjs</body></html>" + ); +} diff --git a/docshell/test/mochitest/file_bug590573_1.html b/docshell/test/mochitest/file_bug590573_1.html new file mode 100644 index 0000000000..b859ca7469 --- /dev/null +++ b/docshell/test/mochitest/file_bug590573_1.html @@ -0,0 +1,7 @@ +<html> +<body onload='opener.page1Load();' onpageshow='opener.page1PageShow();'> + +<div style='height:10000px' id='div1'>This is a very tall div.</div> + +</body> +</html> diff --git a/docshell/test/mochitest/file_bug590573_2.html b/docshell/test/mochitest/file_bug590573_2.html new file mode 100644 index 0000000000..5f9ca22be4 --- /dev/null +++ b/docshell/test/mochitest/file_bug590573_2.html @@ -0,0 +1,8 @@ +<html> +<body onpopstate='opener.page2Popstate();' onload='opener.page2Load();' + onpageshow='opener.page2PageShow();'> + +<div style='height:300%' id='div2'>The second page also has a big div.</div> + +</body> +</html> diff --git a/docshell/test/mochitest/file_bug598895_1.html b/docshell/test/mochitest/file_bug598895_1.html new file mode 100644 index 0000000000..d21f2b4a5d --- /dev/null +++ b/docshell/test/mochitest/file_bug598895_1.html @@ -0,0 +1 @@ +<script>window.onload = function() { opener.postMessage("loaded", "*"); };</script><body>Should show</body> diff --git a/docshell/test/mochitest/file_bug598895_2.html b/docshell/test/mochitest/file_bug598895_2.html new file mode 100644 index 0000000000..680c9bf22b --- /dev/null +++ b/docshell/test/mochitest/file_bug598895_2.html @@ -0,0 +1 @@ +<script>window.onload = function() { opener.postMessage("loaded", "*"); };</script><body></body> diff --git a/docshell/test/mochitest/file_bug634834.html b/docshell/test/mochitest/file_bug634834.html new file mode 100644 index 0000000000..3ff0897451 --- /dev/null +++ b/docshell/test/mochitest/file_bug634834.html @@ -0,0 +1,5 @@ +<html> +<body> +Nothing to see here; just an empty page. +</body> +</html> diff --git a/docshell/test/mochitest/file_bug637644_1.html b/docshell/test/mochitest/file_bug637644_1.html new file mode 100644 index 0000000000..d21f2b4a5d --- /dev/null +++ b/docshell/test/mochitest/file_bug637644_1.html @@ -0,0 +1 @@ +<script>window.onload = function() { opener.postMessage("loaded", "*"); };</script><body>Should show</body> diff --git a/docshell/test/mochitest/file_bug637644_2.html b/docshell/test/mochitest/file_bug637644_2.html new file mode 100644 index 0000000000..680c9bf22b --- /dev/null +++ b/docshell/test/mochitest/file_bug637644_2.html @@ -0,0 +1 @@ +<script>window.onload = function() { opener.postMessage("loaded", "*"); };</script><body></body> diff --git a/docshell/test/mochitest/file_bug640387.html b/docshell/test/mochitest/file_bug640387.html new file mode 100644 index 0000000000..3a939fb41e --- /dev/null +++ b/docshell/test/mochitest/file_bug640387.html @@ -0,0 +1,26 @@ +<html> +<body onhashchange='hashchange()' onload='load()' onpopstate='popstate()'> + +<script> +function hashchange() { + var f = (opener || parent).childHashchange; + if (f) + f(); +} + +function load() { + var f = (opener || parent).childLoad; + if (f) + f(); +} + +function popstate() { + var f = (opener || parent).childPopstate; + if (f) + f(); +} +</script> + +Not much to see here... +</body> +</html> diff --git a/docshell/test/mochitest/file_bug653741.html b/docshell/test/mochitest/file_bug653741.html new file mode 100644 index 0000000000..3202b52573 --- /dev/null +++ b/docshell/test/mochitest/file_bug653741.html @@ -0,0 +1,13 @@ +<html> +<body onload='(parent || opener).childLoad()'> + +<div style='height:500px; background:yellow'> +<a id='#top'>Top of the page</a> +</div> + +<div id='bottom'> +<a id='#bottom'>Bottom of the page</a> +</div> + +</body> +</html> diff --git a/docshell/test/mochitest/file_bug660404 b/docshell/test/mochitest/file_bug660404 new file mode 100644 index 0000000000..ed773420ef --- /dev/null +++ b/docshell/test/mochitest/file_bug660404 @@ -0,0 +1,13 @@ +--testingtesting +Content-Type: text/html + +<script> + var bc = new BroadcastChannel("bug660404_multipart"); + bc.postMessage({command: "finishTest", + textContent: window.document.documentElement.textContent, + innerHTML: window.document.documentElement.innerHTML + }); + bc.close(); + window.close(); +</script> +--testingtesting-- diff --git a/docshell/test/mochitest/file_bug660404-1.html b/docshell/test/mochitest/file_bug660404-1.html new file mode 100644 index 0000000000..878bd80426 --- /dev/null +++ b/docshell/test/mochitest/file_bug660404-1.html @@ -0,0 +1,12 @@ +<script> + var bc = new BroadcastChannel("bug660404"); + window.onload = function() { + setTimeout(() => { + window.onpagehide = function(ev) { + bc.postMessage({command: "pagehide", persisted: ev.persisted}); + bc.close(); + }; + window.location.href = "file_bug660404"; + }, 0); + }; +</script> diff --git a/docshell/test/mochitest/file_bug660404^headers^ b/docshell/test/mochitest/file_bug660404^headers^ new file mode 100644 index 0000000000..5c821f3f48 --- /dev/null +++ b/docshell/test/mochitest/file_bug660404^headers^ @@ -0,0 +1 @@ +Content-Type: multipart/x-mixed-replace; boundary="testingtesting" diff --git a/docshell/test/mochitest/file_bug662170.html b/docshell/test/mochitest/file_bug662170.html new file mode 100644 index 0000000000..3202b52573 --- /dev/null +++ b/docshell/test/mochitest/file_bug662170.html @@ -0,0 +1,13 @@ +<html> +<body onload='(parent || opener).childLoad()'> + +<div style='height:500px; background:yellow'> +<a id='#top'>Top of the page</a> +</div> + +<div id='bottom'> +<a id='#bottom'>Bottom of the page</a> +</div> + +</body> +</html> diff --git a/docshell/test/mochitest/file_bug668513.html b/docshell/test/mochitest/file_bug668513.html new file mode 100644 index 0000000000..ae417a35bd --- /dev/null +++ b/docshell/test/mochitest/file_bug668513.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test file for Bug 668513</title> +<script> + var SimpleTest = opener.SimpleTest; + var ok = opener.ok; + var is = opener.is; + + function finish() { + SimpleTest.finish(); + close(); + } + + function onload_test() { + var win = frames[0]; + ok(win.performance, "Window.performance should be defined"); + ok(win.performance.navigation, "Window.performance.navigation should be defined"); + var navigation = win.performance && win.performance.navigation; + if (navigation === undefined) { + // avoid script errors + finish(); + return; + } + + // do this with a timeout to see the visuals of the navigations. + setTimeout(nav_frame, 100); + } + + var step = 1; + function nav_frame() { + var navigation_frame = frames[0]; + var navigation = navigation_frame.performance.navigation; + switch (step) { + case 1: + { + navigation_frame.location.href = "bug570341_recordevents.html"; + step++; + break; + } + case 2: + { + is(navigation.type, navigation.TYPE_NAVIGATE, + "Expected window.performance.navigation.type == TYPE_NAVIGATE"); + navigation_frame.history.back(); + step++; + break; + } + case 3: + { + is(navigation.type, navigation.TYPE_BACK_FORWARD, + "Expected window.performance.navigation.type == TYPE_BACK_FORWARD"); + step++; + navigation_frame.history.forward(); + break; + } + case 4: + { + is(navigation.type, navigation.TYPE_BACK_FORWARD, + "Expected window.performance.navigation.type == TYPE_BACK_FORWARD"); + navigation_frame.location.href = "bug668513_redirect.html"; + step++; + break; + } + case 5: + { + is(navigation.type, navigation.TYPE_NAVIGATE, + "Expected timing.navigation.type as TYPE_NAVIGATE"); + is(navigation.redirectCount, 1, + "Expected navigation.redirectCount == 1 on an server redirected navigation"); + + var timing = navigation_frame.performance && navigation_frame.performance.timing; + if (timing === undefined) { + // avoid script errors + finish(); + break; + } + ok(timing.navigationStart > 0, "navigationStart should be > 0"); + var sequence = ["navigationStart", "redirectStart", "redirectEnd", "fetchStart"]; + for (var j = 1; j < sequence.length; ++j) { + var prop = sequence[j]; + var prevProp = sequence[j - 1]; + ok(timing[prevProp] <= timing[prop], + ["Expected ", prevProp, " to happen before ", prop, + ", got ", prevProp, " = ", timing[prevProp], + ", ", prop, " = ", timing[prop]].join("")); + } + step++; + finish(); + break; + } + } + } +</script> +</head> +<body> +<div id="frames"> +<iframe name="child0" onload="onload_test();" src="navigation/blank.html"></iframe> +</div> +</body> +</html> diff --git a/docshell/test/mochitest/file_bug669671.sjs b/docshell/test/mochitest/file_bug669671.sjs new file mode 100644 index 0000000000..5871419de8 --- /dev/null +++ b/docshell/test/mochitest/file_bug669671.sjs @@ -0,0 +1,17 @@ +function handleRequest(request, response) { + var count = parseInt(getState("count")); + if (!count || request.queryString == "countreset") { + count = 0; + } + + setState("count", count + 1 + ""); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "max-age=0"); + response.write( + '<html><body onload="opener.onChildLoad()" ' + + "onunload=\"parseInt('0')\">" + + count + + "</body></html>" + ); +} diff --git a/docshell/test/mochitest/file_bug675587.html b/docshell/test/mochitest/file_bug675587.html new file mode 100644 index 0000000000..842ab9ae79 --- /dev/null +++ b/docshell/test/mochitest/file_bug675587.html @@ -0,0 +1 @@ +<script>location.hash = "";</script> diff --git a/docshell/test/mochitest/file_bug680257.html b/docshell/test/mochitest/file_bug680257.html new file mode 100644 index 0000000000..ff480e96a5 --- /dev/null +++ b/docshell/test/mochitest/file_bug680257.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <style type='text/css'> + a { color: black; } + a:target { color: red; } + </style> +</head> + +<body onload='(opener || parent).popupLoaded()'> + +<a id='a' href='#a'>link</a> +<a id='b' href='#b'>link2</a> + +</body> +</html> diff --git a/docshell/test/mochitest/file_bug703855.html b/docshell/test/mochitest/file_bug703855.html new file mode 100644 index 0000000000..fe15b6e3df --- /dev/null +++ b/docshell/test/mochitest/file_bug703855.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<!-- Just need an empty file here, as long as it's served over HTTP --> diff --git a/docshell/test/mochitest/file_bug728939.html b/docshell/test/mochitest/file_bug728939.html new file mode 100644 index 0000000000..1cd52a44e1 --- /dev/null +++ b/docshell/test/mochitest/file_bug728939.html @@ -0,0 +1,3 @@ +<html> +<body onload="opener.popupLoaded()">file_bug728939</body> +</html> diff --git a/docshell/test/mochitest/file_close_onpagehide1.html b/docshell/test/mochitest/file_close_onpagehide1.html new file mode 100644 index 0000000000..ccf3b625a1 --- /dev/null +++ b/docshell/test/mochitest/file_close_onpagehide1.html @@ -0,0 +1,5 @@ +<script> + window.onload = () => { + opener.postMessage("initial", "*"); + }; +</script> diff --git a/docshell/test/mochitest/file_close_onpagehide2.html b/docshell/test/mochitest/file_close_onpagehide2.html new file mode 100644 index 0000000000..a8e9479f47 --- /dev/null +++ b/docshell/test/mochitest/file_close_onpagehide2.html @@ -0,0 +1,5 @@ +<script> + window.onload = () => { + opener.postMessage("second", "*"); + }; +</script>; diff --git a/docshell/test/mochitest/file_compressed_multipart b/docshell/test/mochitest/file_compressed_multipart Binary files differnew file mode 100644 index 0000000000..3c56226951 --- /dev/null +++ b/docshell/test/mochitest/file_compressed_multipart diff --git a/docshell/test/mochitest/file_compressed_multipart^headers^ b/docshell/test/mochitest/file_compressed_multipart^headers^ new file mode 100644 index 0000000000..9376927812 --- /dev/null +++ b/docshell/test/mochitest/file_compressed_multipart^headers^ @@ -0,0 +1,2 @@ +Content-Type: multipart/x-mixed-replace; boundary="testingtesting" +Content-Encoding: gzip diff --git a/docshell/test/mochitest/file_content_javascript_loads_frame.html b/docshell/test/mochitest/file_content_javascript_loads_frame.html new file mode 100644 index 0000000000..9e2851aa8b --- /dev/null +++ b/docshell/test/mochitest/file_content_javascript_loads_frame.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<script type="application/javascript"> +"use strict"; + +addEventListener("message", event => { + if ("ping" in event.data) { + event.source.postMessage({ pong: event.data.ping }, event.origin); + } +}); +</script> +</head> +<body> +</body> +</html> diff --git a/docshell/test/mochitest/file_content_javascript_loads_root.html b/docshell/test/mochitest/file_content_javascript_loads_root.html new file mode 100644 index 0000000000..b9f2c1faa7 --- /dev/null +++ b/docshell/test/mochitest/file_content_javascript_loads_root.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<script type="application/javascript"> +"use strict"; + +window.onload = () => { + opener.postMessage("ready", "*"); +}; + +// eslint-disable-next-line no-shadow +function promiseMessage(source, filter = event => true) { + return new Promise(resolve => { + function listener(event) { + if (event.source == source && filter(event)) { + removeEventListener("message", listener); + resolve(event); + } + } + addEventListener("message", listener); + }); +} + +// Sends a message to the given target window and waits for the response. +function ping(target) { + let msg = { ping: Math.random() }; + target.postMessage(msg, "*"); + return promiseMessage( + target, + event => event.data && event.data.pong == msg.ping + ); +} + +function setFrameLocation(name, uri) { + window[name].location = uri; +} +</script> +</head> +<body> +</body> +</html> diff --git a/docshell/test/mochitest/file_form_restoration_no_store.html b/docshell/test/mochitest/file_form_restoration_no_store.html new file mode 100644 index 0000000000..6e8756a693 --- /dev/null +++ b/docshell/test/mochitest/file_form_restoration_no_store.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script> + window.addEventListener("pageshow", ({ persisted }) => { + let bc = new BroadcastChannel("form_restoration"); + bc.addEventListener("message", ({ data }) => { + switch (data) { + case "enter_data": + document.getElementById("formElement").value = "test"; + break; + case "reload": + bc.close(); + location.reload(); + break; + case "navigate": + bc.close(); + document.location = "file_form_restoration_no_store.html?1"; + break; + case "back": + bc.close(); + history.back(); + break; + case "close": + bc.close(); + window.close(); + break; + } + }); + bc.postMessage({ persisted, formData: document.getElementById("formElement").value }); + }); + </script> +</head> +<body> + <input id="formElement" type="text" value="initial"> +</body> +</html> diff --git a/docshell/test/mochitest/file_form_restoration_no_store.html^headers^ b/docshell/test/mochitest/file_form_restoration_no_store.html^headers^ new file mode 100644 index 0000000000..4030ea1d3d --- /dev/null +++ b/docshell/test/mochitest/file_form_restoration_no_store.html^headers^ @@ -0,0 +1 @@ +Cache-Control: no-store diff --git a/docshell/test/mochitest/file_framedhistoryframes.html b/docshell/test/mochitest/file_framedhistoryframes.html new file mode 100644 index 0000000000..314f9c72d8 --- /dev/null +++ b/docshell/test/mochitest/file_framedhistoryframes.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<body> +<iframe id="iframe" src="historyframes.html"></iframe> +<script type="application/javascript"> + +var SimpleTest = window.opener.SimpleTest; +var is = window.opener.is; + +function done() { + window.opener.done(); +} + +</script> +</body> +</html> diff --git a/docshell/test/mochitest/file_load_during_reload.html b/docshell/test/mochitest/file_load_during_reload.html new file mode 100644 index 0000000000..600d5c1728 --- /dev/null +++ b/docshell/test/mochitest/file_load_during_reload.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> + <head> + <script> + function notifyOpener() { + opener.postMessage("loaded", "*"); + } + </script> + </head> + <body onload="notifyOpener()"> + </body> +</html> diff --git a/docshell/test/mochitest/file_pushState_after_document_open.html b/docshell/test/mochitest/file_pushState_after_document_open.html new file mode 100644 index 0000000000..97a6954f2e --- /dev/null +++ b/docshell/test/mochitest/file_pushState_after_document_open.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + document.addEventListener("DOMContentLoaded", function() { + document.open(); + document.write("<!DOCTYPE html>New Document here"); + document.close(); + // Notify parent via postMessage, since otherwise exceptions will not get + // caught by its onerror handler. + parent.postMessage("doTest", "*"); + }); +</script> diff --git a/docshell/test/mochitest/file_redirect_history.html b/docshell/test/mochitest/file_redirect_history.html new file mode 100644 index 0000000000..3971faf4fd --- /dev/null +++ b/docshell/test/mochitest/file_redirect_history.html @@ -0,0 +1,18 @@ +<html> + <head> + <script> + function loaded() { + addEventListener("message", ({ data }) => { + document.getElementById("form").action = data; + document.getElementById("button").click(); + }, { once: true }); + opener.postMessage("loaded", "*"); + } + </script> + </head> + <body onload="loaded();"> + <form id="form" method="POST"> + <input id="button" type="submit" /> + </form> + </body> +</html> diff --git a/docshell/test/mochitest/form_submit.sjs b/docshell/test/mochitest/form_submit.sjs new file mode 100644 index 0000000000..1a1fa5d89c --- /dev/null +++ b/docshell/test/mochitest/form_submit.sjs @@ -0,0 +1,40 @@ +"use strict"; + +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +const BinaryOutputStream = CC( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" +); + +function log(str) { + // dump(`LOG: ${str}\n`); +} + +async function handleRequest(request, response) { + if (request.method !== "POST") { + throw new Error("Expected a post request"); + } else { + log("Reading request"); + let available = 0; + let inputStream = new BinaryInputStream(request.bodyInputStream); + while ((available = inputStream.available()) > 0) { + log(inputStream.readBytes(available)); + } + } + + log("Setting Headers"); + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(request.httpVersion, "200", "OK"); + log("Writing body"); + response.write( + '<script>"use strict"; let target = opener ? opener : parent; target.postMessage("done", "*");</script>' + ); + log("Done"); +} diff --git a/docshell/test/mochitest/form_submit_redirect.sjs b/docshell/test/mochitest/form_submit_redirect.sjs new file mode 100644 index 0000000000..dbc32b9643 --- /dev/null +++ b/docshell/test/mochitest/form_submit_redirect.sjs @@ -0,0 +1,15 @@ +"use strict"; + +Cu.importGlobalProperties(["URLSearchParams"]); + +async function handleRequest(request, response) { + if (request.method !== "POST") { + throw new Error("Expected a post request"); + } else { + let params = new URLSearchParams(request.queryString); + let redirect = params.get("redirectTo"); + + response.setStatusLine(request.httpVersion, 302, "Moved Temporarily"); + response.setHeader("Location", redirect); + } +} diff --git a/docshell/test/mochitest/historyframes.html b/docshell/test/mochitest/historyframes.html new file mode 100644 index 0000000000..31f46a5071 --- /dev/null +++ b/docshell/test/mochitest/historyframes.html @@ -0,0 +1,176 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=602256 +--> +<head> + <title>Test for Bug 602256</title> +</head> +<body onload="SimpleTest.executeSoon(run_test)"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602256">Mozilla Bug 602256</a> +<div id="content"> + <iframe id="iframe" src="start_historyframe.html"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 602256 */ + +var testWin = window.opener ? window.opener : window.parent; + +var SimpleTest = testWin.SimpleTest; +function is() { testWin.is.apply(testWin, arguments); } + +var gFrame = null; + +function gState() { + return location.hash.replace(/^#/, ""); +} + +function waitForLoad(aCallback) { + function listener() { + gFrame.removeEventListener("load", listener); + SimpleTest.executeSoon(aCallback); + } + + gFrame.addEventListener("load", listener); +} + +function loadContent(aURL, aCallback) { + waitForLoad(aCallback); + + gFrame.src = aURL; +} + +function getURL() { + return gFrame.contentDocument.documentURI; +} + +function getContent() { + return gFrame.contentDocument.getElementById("text").textContent; +} + +var BASE_URI = "http://mochi.test:8888/tests/docshell/test/mochitest/"; +var START = BASE_URI + "start_historyframe.html"; +var URL1 = BASE_URI + "url1_historyframe.html"; +var URL2 = BASE_URI + "url2_historyframe.html"; + +function run_test() { + window.location.hash = "START"; + + gFrame = document.getElementById("iframe"); + + test_basic_inner_navigation(); +} + +function end_test() { + testWin.done(); +} + +var gTestContinuation = null; +function continueAsync() { + setTimeout(function() { gTestContinuation.next(); }) +} + +function test_basic_inner_navigation() { + // Navigate the inner frame a few times + loadContent(URL1, function() { + is(getURL(), URL1, "URL should be correct"); + is(getContent(), "Test1", "Page should be correct"); + + loadContent(URL2, function() { + is(getURL(), URL2, "URL should be correct"); + is(getContent(), "Test2", "Page should be correct"); + + // Test that history is working + waitForLoad(function() { + is(getURL(), URL1, "URL should be correct"); + is(getContent(), "Test1", "Page should be correct"); + + waitForLoad(function() { + is(getURL(), URL2, "URL should be correct"); + is(getContent(), "Test2", "Page should be correct"); + + gTestContinuation = test_state_navigation(); + gTestContinuation.next(); + }); + window.history.forward(); + }); + window.history.back(); + }); + }); +} + +function* test_state_navigation() { + window.location.hash = "STATE1"; + + is(getURL(), URL2, "URL should be correct"); + is(getContent(), "Test2", "Page should be correct"); + + window.location.hash = "STATE2"; + + is(getURL(), URL2, "URL should be correct"); + is(getContent(), "Test2", "Page should be correct"); + + window.addEventListener("popstate", (e) => { + continueAsync(); + }, {once: true}); + window.history.back(); + yield; + + is(gState(), "STATE1", "State should be correct after going back"); + is(getURL(), URL2, "URL should be correct"); + is(getContent(), "Test2", "Page should be correct"); + + window.addEventListener("popstate", (e) => { + continueAsync(); + }, {once: true}); + window.history.forward(); + yield; + + is(gState(), "STATE2", "State should be correct after going forward"); + is(getURL(), URL2, "URL should be correct"); + is(getContent(), "Test2", "Page should be correct"); + + window.addEventListener("popstate", (e) => { + continueAsync(); + }, {once: true}); + window.history.back(); + yield; + + window.addEventListener("popstate", (e) => { + continueAsync(); + }, {once: true}); + window.history.back(); + yield; + + is(gState(), "START", "State should be correct"); + is(getURL(), URL2, "URL should be correct"); + is(getContent(), "Test2", "Page should be correct"); + + waitForLoad(function() { + is(getURL(), URL1, "URL should be correct"); + is(getContent(), "Test1", "Page should be correct"); + + waitForLoad(function() { + is(gState(), "START", "State should be correct"); + is(getURL(), START, "URL should be correct"); + is(getContent(), "Start", "Page should be correct"); + + end_test(); + }); + + window.history.back(); + + is(gState(), "START", "State should be correct after going back twice"); + }); + + window.history.back(); + continueAsync(); + yield; + is(gState(), "START", "State should be correct"); +} +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/mochitest.ini b/docshell/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..598d629673 --- /dev/null +++ b/docshell/test/mochitest/mochitest.ini @@ -0,0 +1,209 @@ +[DEFAULT] +support-files = + bug404548-subframe.html + bug404548-subframe_window.html + bug413310-post.sjs + bug413310-subframe.html + bug529119-window.html + bug570341_recordevents.html + bug668513_redirect.html + bug668513_redirect.html^headers^ + bug691547_frame.html + dummy_page.html + file_anchor_scroll_after_document_open.html + file_bfcache_plus_hash_1.html + file_bfcache_plus_hash_2.html + file_bug385434_1.html + file_bug385434_2.html + file_bug385434_3.html + file_bug475636.sjs + file_bug509055.html + file_bug540462.html + file_bug580069_1.html + file_bug580069_2.sjs + file_bug598895_1.html + file_bug598895_2.html + file_bug590573_1.html + file_bug590573_2.html + file_bug634834.html + file_bug637644_1.html + file_bug637644_2.html + file_bug640387.html + file_bug653741.html + file_bug660404 + file_bug660404^headers^ + file_bug660404-1.html + file_bug662170.html + file_bug669671.sjs + file_bug680257.html + file_bug703855.html + file_bug728939.html + file_bug1121701_1.html + file_bug1121701_2.html + file_bug1186774.html + file_bug1151421.html + file_bug1450164.html + file_close_onpagehide1.html + file_close_onpagehide2.html + file_compressed_multipart + file_compressed_multipart^headers^ + file_pushState_after_document_open.html + historyframes.html + ping.html + start_historyframe.html + url1_historyframe.html + url2_historyframe.html + +[test_anchor_scroll_after_document_open.html] +[test_bfcache_plus_hash.html] +[test_bug1422334.html] +support-files = + bug1422334_redirect.html + bug1422334_redirect.html^headers^ + !/docshell/test/navigation/blank.html +[test_bug385434.html] +[test_bug387979.html] +[test_bug402210.html] +skip-if = + http3 +[test_bug404548.html] +[test_bug413310.html] +skip-if = true +# Disabled for too many intermittent failures (bug 719186) +[test_bug475636.html] +[test_bug509055.html] +[test_bug511449.html] +skip-if = toolkit != "cocoa" || headless # Headless: bug 1410525 +support-files = file_bug511449.html +[test_bug529119-1.html] +skip-if = + fission && os == "android" # Bug 1827321 +[test_bug529119-2.html] +skip-if = + http3 + fission && os == "android" # Bug 1827321 +[test_bug530396.html] +support-files = bug530396-noref.sjs bug530396-subframe.html +skip-if = + http3 +[test_bug540462.html] +skip-if = toolkit == 'android' && debug +[test_bug551225.html] +[test_bug570341.html] +skip-if = (verify && !debug && (os == 'win')) +[test_bug580069.html] +skip-if = (verify && !debug && (os == 'win')) +[test_bug590573.html] +[test_bug598895.html] +[test_bug634834.html] +skip-if = + http3 +[test_bug637644.html] +[test_bug640387_1.html] +[test_bug640387_2.html] +[test_bug653741.html] +[test_bug660404.html] +[test_bug662170.html] +[test_bug668513.html] +support-files = file_bug668513.html +[test_bug669671.html] +[test_bug675587.html] +support-files = file_bug675587.html +[test_bug680257.html] +[test_bug691547.html] +[test_bug694612.html] +[test_bug703855.html] +[test_bug728939.html] +[test_bug797909.html] +[test_bug1045096.html] +[test_bug1121701.html] +[test_bug1151421.html] +[test_bug1186774.html] +[test_bug1450164.html] +[test_bug1507702.html] +[test_bug1645781.html] +support-files = + form_submit.sjs +skip-if = + http3 +[test_bug1729662.html] +support-files = + file_bug1729662.html +[test_bug1740516.html] +support-files = + file_bug1740516_1.html + file_bug1740516_1_inner.html + file_bug1740516_2.html +[test_form_restoration.html] +support-files = + file_form_restoration_no_store.html + file_form_restoration_no_store.html^headers^ +[test_bug1741132.html] +support-files = + file_bug1741132.html +skip-if = toolkit == "android" && !sessionHistoryInParent +[test_bug1742865.html] +support-files = + file_bug1742865.sjs + file_bug1742865_outer.sjs +skip-if = + toolkit == "android" && debug && fission && verify # Bug 1745937 + http3 +[test_bug1743353.html] +support-files = + file_bug1743353.html +[test_bug1747033.html] +support-files = + file_bug1747033.sjs +skip-if = + http3 +[test_bug1773192.html] +support-files = + file_bug1773192_1.html + file_bug1773192_2.html + file_bug1773192_3.sjs +[test_close_onpagehide_by_history_back.html] +[test_close_onpagehide_by_window_close.html] +[test_compressed_multipart.html] +[test_content_javascript_loads.html] +support-files = + file_content_javascript_loads_root.html + file_content_javascript_loads_frame.html +skip-if = + http3 +[test_forceinheritprincipal_overrule_owner.html] +skip-if = + http3 +[test_framedhistoryframes.html] +support-files = file_framedhistoryframes.html +skip-if = + http3 +[test_load_during_reload.html] +support-files = file_load_during_reload.html +[test_pushState_after_document_open.html] +[test_navigate_after_pagehide.html] +skip-if = + http3 +[test_redirect_history.html] +support-files = + file_redirect_history.html + form_submit_redirect.sjs +skip-if = + http3 +[test_windowedhistoryframes.html] +skip-if = + (!debug && os == 'android') # Bug 1573892 + http3 +[test_triggeringprincipal_location_seturi.html] +skip-if = + http3 +[test_double_submit.html] +support-files = + clicker.html + double_submit.sjs +skip-if = + http3 +[test_iframe_srcdoc_to_remote.html] +skip-if = + http3 +[test_javascript_sandboxed_popup.html] diff --git a/docshell/test/mochitest/ping.html b/docshell/test/mochitest/ping.html new file mode 100644 index 0000000000..7d84560dd1 --- /dev/null +++ b/docshell/test/mochitest/ping.html @@ -0,0 +1,6 @@ +<!doctype html> +<script> + "use strict"; + let target = (window.opener || window.parent); + target.postMessage("ping", "*"); +</script> diff --git a/docshell/test/mochitest/start_historyframe.html b/docshell/test/mochitest/start_historyframe.html new file mode 100644 index 0000000000..a791af4e64 --- /dev/null +++ b/docshell/test/mochitest/start_historyframe.html @@ -0,0 +1 @@ +<p id='text'>Start</p> diff --git a/docshell/test/mochitest/test_anchor_scroll_after_document_open.html b/docshell/test/mochitest/test_anchor_scroll_after_document_open.html new file mode 100644 index 0000000000..5309f6cf19 --- /dev/null +++ b/docshell/test/mochitest/test_anchor_scroll_after_document_open.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=881487 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 881487</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 881487 */ + SimpleTest.waitForExplicitFinish(); + // Child needs to invoke us, otherwise our onload will fire before the child + // has done the write/close bit. + var gotOnload = false; + addLoadEvent(function() { + gotOnload = true; + }); + onmessage = function handleMessage(msg) { + if (msg.data == "doTest") { + if (!gotOnload) { + addLoadEvent(function() { handleMessage(msg); }); + return; + } + frames[0].onscroll = function() { + ok(true, "Got a scroll event"); + SimpleTest.finish(); + }; + frames[0].location.hash = "#target"; + return; + } + if (msg.data == "haveHash") { + ok(false, "Child got reloaded"); + } else { + ok(false, "Unexpected message"); + } + SimpleTest.finish(); + }; + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=881487">Mozilla Bug 881487</a> +<p id="display"> + <!-- iframe goes here so it can scroll --> +<iframe src="file_anchor_scroll_after_document_open.html"></iframe> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bfcache_plus_hash.html b/docshell/test/mochitest/test_bfcache_plus_hash.html new file mode 100644 index 0000000000..cb5bc06f21 --- /dev/null +++ b/docshell/test/mochitest/test_bfcache_plus_hash.html @@ -0,0 +1,153 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=646641 +--> +<head> + <title>Test for Bug 646641</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=646641">Mozilla Bug 646641</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 646641 */ + +/** + * Steps: + * - Main page (this one) opens file_bfcache_plus_hash_1.html (subpage 1) + * - subpage 1 sends msg { "childLoad", 1 } + * - subpage 1 sends msg { "childPageshow", 1 } + * - main page sends message "pushState" + * - subpage 1 does pushState() + * - subpage 1 navigates to file_bfcache_plus_hash_2.html (subpage 2) + * - subpage 2 sends msg { "childLoad", 2 } + * - subpage 2 sends msg { "childPageshow", 2 } + * - main page sends msg "go-2" + * - subpage 2 goes back two history entries + * - subpage 1 sends msg { "childPageshow", 1 } + * - Receiving only this msg shows we have retrieved the document from bfcache + * - main page sends msg "close" + * - subpage 1 sends msg "closed" + */ +SimpleTest.waitForExplicitFinish(); + +function debug(msg) { + // Wrap dump so we can turn debug messages on and off easily. + dump(msg + "\n"); +} + +var expectedLoadNum = -1; +var expectedPageshowNum = -1; + +function waitForLoad(n) { + debug("Waiting for load " + n); + expectedLoadNum = n; +} + +function waitForShow(n) { + debug("Waiting for show " + n); + expectedPageshowNum = n; +} + + + +function executeTest() { + function* test() { + window.open("file_bfcache_plus_hash_1.html", "", "noopener"); + waitForLoad(1); + waitForShow(1); + yield undefined; + yield undefined; + + bc1.postMessage("pushState"); + + waitForLoad(2); + waitForShow(2); + yield undefined; + yield undefined; + + // Now go back 2. The first page should be retrieved from bfcache. + bc2.postMessage("go-2"); + waitForShow(1); + yield undefined; + + bc1.postMessage("close"); + } + + var bc1 = new BroadcastChannel("bug646641_1"); + var bc2 = new BroadcastChannel("bug646641_2"); + bc1.onmessage = (msgEvent) => { + var msg = msgEvent.data.message; + var n = msgEvent.data.num; + if (msg == "childLoad") { + if (n == expectedLoadNum) { + debug("Got load " + n); + expectedLoadNum = -1; + + // Spin the event loop before calling gGen.next() so the generator runs + // outside the onload handler. This prevents us from encountering all + // sorts of docshell quirks. + setTimeout(function() { gGen.next(); }, 0); + } else { + debug("Got unexpected load " + n); + ok(false, "Got unexpected load " + n); + } + } else if (msg == "childPageshow") { + if (n == expectedPageshowNum) { + debug("Got expected pageshow " + n); + expectedPageshowNum = -1; + ok(true, "Got expected pageshow " + n); + setTimeout(function() { gGen.next(); }, 0); + } else { + debug("Got unexpected pageshow " + n); + ok(false, "Got unexpected pageshow " + n); + } + } else if (msg == "closed") { + bc1.close(); + bc2.close(); + SimpleTest.finish(); + } + } + + bc2.onmessage = bc1.onmessage; + + var gGen = test(); + + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + gGen.next(); + }); +} +if (isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + SpecialPowers.addPermission("storageAccessAPI", true, window.location.href).then(() => { + SpecialPowers.wrap(document).requestStorageAccess().then(() => { + SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]], + }).then(() => { + executeTest(); + }); + }); + }); +} else { + executeTest(); +} + + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1045096.html b/docshell/test/mochitest/test_bug1045096.html new file mode 100644 index 0000000000..2df2232b7e --- /dev/null +++ b/docshell/test/mochitest/test_bug1045096.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1045096 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1045096</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045096">Mozilla Bug 1045096</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + <script type="application/javascript"> + + /** Test for Bug 1045096 */ + var i = document.createElement("iframe"); + i.src = "javascript:false"; // This is required! + $("content").appendChild(i); + ok(i.contentWindow.performance, "Should have a performance object"); + </script> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1121701.html b/docshell/test/mochitest/test_bug1121701.html new file mode 100644 index 0000000000..cd0b2529d4 --- /dev/null +++ b/docshell/test/mochitest/test_bug1121701.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1121701 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1121701</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1121701 */ + SimpleTest.waitForExplicitFinish(); + + var testUrl1 = "file_bug1121701_1.html"; + var testUrl2 = "file_bug1121701_2.html"; + + var page1LoadCount = 0; + let page1Done = {}; + page1Done.promise = new Promise(resolve => { + page1Done.resolve = resolve; + }); + let page2Done = {}; + page2Done.promise = new Promise(resolve => { + page2Done.resolve = resolve; + }); + + addLoadEvent(async function() { + + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + if (isXOrigin) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]], + }); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await SpecialPowers.addPermission("storageAccessAPI", true, window.location.href); + await SpecialPowers.wrap(document).requestStorageAccess(); + } + + var bc = new BroadcastChannel("file_bug1121701_1"); + var bc2 = new BroadcastChannel("file_bug1121701_2"); + + async function scheduleFinish() { + await Promise.all([page1Done.promise, page2Done.promise]); + bc2.close(); + bc.close(); + SimpleTest.finish(); + } + + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "child1PageShow") { + ++page1LoadCount; + var persisted = msg.persisted; + var pageHideAsserts = msg.pageHideAsserts; + if (pageHideAsserts) { + ok(pageHideAsserts.persisted, "onpagehide: test page 1 should get persisted"); + is(pageHideAsserts.innerHTML, "modified", "onpagehide: innerHTML text is 'modified'"); + } + if (page1LoadCount == 1) { + SimpleTest.executeSoon(function() { + is(persisted, false, "Initial page load shouldn't be persisted."); + bc.postMessage({command: "setInnerHTML", testUrl2}); + }); + } else if (page1LoadCount == 2) { + is(persisted, true, "Page load from bfcache should be persisted."); + is(msg.innerHTML, "modified", "innerHTML text is 'modified'"); + bc.postMessage({command: "close"}); + } + } else if (command == "closed") { + page1Done.resolve(); + } + } + bc2.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "child2PageShow") { + bc2.postMessage({command: "setInnerHTML", location: location.href}); + } else if (command == "onmessage") { + page2Done.resolve(); + } + } + + scheduleFinish(); + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + window.open(testUrl1, "", "noopener"); + }); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1121701">Mozilla Bug 1121701</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1151421.html b/docshell/test/mochitest/test_bug1151421.html new file mode 100644 index 0000000000..0738ee783e --- /dev/null +++ b/docshell/test/mochitest/test_bug1151421.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1151421 +--> +<head> + <title>Test for Bug 1151421</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151421">Mozilla Bug 1151421</a> + +<script type="application/javascript"> + +/** Test for Bug 1151421 */ +SimpleTest.waitForExplicitFinish(); + +function childLoad() { + // Spin the event loop so we leave the onload handler. + SimpleTest.executeSoon(childLoad2); +} + +function childLoad2() { + let iframe = document.getElementById("iframe"); + let cw = iframe.contentWindow; + let content = cw.document.getElementById("content"); + + // Create a function to calculate an invariant. + let topPlusOffset = function() { + return Math.round(content.getBoundingClientRect().top + cw.pageYOffset); + }; + + let initialTPO = topPlusOffset(); + + // Scroll the iframe to various positions, and check the TPO. + // Scrolling down to the bottom will adjust the page offset by a fractional amount. + let positions = [-100, 0.17, 0, 1.5, 10.41, 1e6, 12.1]; + + // Run some tests with scrollTo() and ensure we have the same invariant after scrolling. + positions.forEach(function(pos) { + cw.scrollTo(0, pos); + is(topPlusOffset(), initialTPO, "Top plus offset should remain invariant across scrolling."); + }); + + positions.reverse().forEach(function(pos) { + cw.scrollTo(0, pos); + is(topPlusOffset(), initialTPO, "(reverse) Top plus offset should remain invariant across scrolling."); + }); + + SimpleTest.finish(); +} + +</script> + +<!-- When the iframe loads, it calls childLoad(). --> +<br> +<iframe height='100px' id='iframe' src='file_bug1151421.html'></iframe> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1186774.html b/docshell/test/mochitest/test_bug1186774.html new file mode 100644 index 0000000000..9ec56baf11 --- /dev/null +++ b/docshell/test/mochitest/test_bug1186774.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1186774 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1186774</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1186774 */ + +var child; + +function runTest() { + child = window.open("file_bug1186774.html", "", "width=100,height=100"); + child.onload = function() { + setTimeout(function() { + child.scrollTo(0, 0); + child.history.pushState({}, "initial"); + child.scrollTo(0, 3000); + child.history.pushState({}, "scrolled"); + child.scrollTo(0, 6000); + child.history.back(); + }); + }; + + child.onpopstate = function() { + is(Math.round(child.scrollY), 6000, "Shouldn't have scrolled before popstate"); + child.close(); + SimpleTest.finish(); + }; +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1186774">Mozilla Bug 1186774</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1422334.html b/docshell/test/mochitest/test_bug1422334.html new file mode 100644 index 0000000000..b525ae1d9c --- /dev/null +++ b/docshell/test/mochitest/test_bug1422334.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Ensure that reload after replaceState after 3xx redirect does the right thing.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + var ifr = document.querySelector("iframe"); + var win = ifr.contentWindow; + is(win.location.href, location.href.replace(location.search, "") + .replace("mochitest/test_bug1422334.html", + "navigation/blank.html?x=y"), + "Should have the right location on initial load"); + + win.history.replaceState(null, '', win.location.pathname); + is(win.location.href, location.href.replace(location.search, "") + .replace("mochitest/test_bug1422334.html", + "navigation/blank.html"), + "Should have the right location after replaceState call"); + + ifr.onload = function() { + is(win.location.href, location.href.replace(location.search, "") + .replace("mochitest/test_bug1422334.html", + "navigation/blank.html"), + "Should have the right location after reload"); + SimpleTest.finish(); + } + win.location.reload(); + }); + </script> +</head> +<body> +<p id="display"><iframe src="bug1422334_redirect.html"></iframe></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1450164.html b/docshell/test/mochitest/test_bug1450164.html new file mode 100644 index 0000000000..546a988394 --- /dev/null +++ b/docshell/test/mochitest/test_bug1450164.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> + <!-- + https://bugzilla.mozilla.org/show_bug.cgi?id=1450164 + --> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1450164</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1450164 */ + + function runTest() { + var child = window.open("file_bug1450164.html", "", "width=100,height=100"); + child.onload = function() { + // After the window loads, close it. If we don't crash in debug, consider that a pass. + child.close(); + }; + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(runTest); + + </script> + </head> + <body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1450164">Mozilla Bug 1450164</a> + </body> +</html> diff --git a/docshell/test/mochitest/test_bug1507702.html b/docshell/test/mochitest/test_bug1507702.html new file mode 100644 index 0000000000..fd88ee60a5 --- /dev/null +++ b/docshell/test/mochitest/test_bug1507702.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1507702 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1507702</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <link rel="icon" href="about:crashparent"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1507702">Mozilla Bug 1507702</a> +<img src="about:crashparent"> +<img src="about:crashcontent"> +<iframe src="about:crashparent"></iframe> +<iframe src="about:crashcontent"></iframe> +<script> + let urls = ["about:crashparent", "about:crashcontent"]; + async function testFetch() { + const url = urls.shift(); + if (!url) { + return Promise.resolve(); + } + + let threw; + try { + await fetch(url); + threw = false; + } catch (e) { + threw = true; + } + + ok(threw === true, "fetch should reject"); + return testFetch(); + } + + document.body.onload = async () => { + for (const url of ["about:crashparent", "about:crashcontent"]) { + SimpleTest.doesThrow(() => { + top.location.href = url; + }, "navigation should throw"); + + SimpleTest.doesThrow(() => { + location.href = url; + }, "navigation should throw"); + } + + await testFetch(); + SimpleTest.finish(); + }; + + SimpleTest.waitForExplicitFinish(); +</script> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1645781.html b/docshell/test/mochitest/test_bug1645781.html new file mode 100644 index 0000000000..6cf676b7a9 --- /dev/null +++ b/docshell/test/mochitest/test_bug1645781.html @@ -0,0 +1,90 @@ +<!doctype html> +<html> + <head> + <title>Test for Bug 1590762</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <form id="form" action="form_submit.sjs" method="POST" target="targetFrame"> + <input id="input" type="text" name="name" value=""> + <input id="button" type="submit"> + </form> + <script> + "use strict"; + const PATH = "/tests/docshell/test/mochitest/"; + const SAME_ORIGIN = new URL(PATH, window.location.origin);; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const CROSS_ORIGIN_1 = new URL(PATH, "http://test1.example.com/"); + const CROSS_ORIGIN_2 = new URL(PATH, "https://example.com/"); + const TARGET = "ping.html"; + const ACTION = "form_submit.sjs"; + + function generateBody(size) { + let data = new Uint8Array(size); + for (let i = 0; i < size; ++i) { + data[i] = 97 + Math.random() * (123 - 97); + } + + return new TextDecoder().decode(data); + } + + async function withFrame(url) { + info("Creating frame"); + let frame = document.createElement('iframe'); + frame.name = "targetFrame"; + + return new Promise(resolve => { + addEventListener('message', async function({source}) { + info("Frame loaded"); + if (frame.contentWindow == source) { + resolve(frame); + } + }, { once: true }); + frame.src = url; + document.body.appendChild(frame); + }); + } + + function click() { + synthesizeMouse(document.getElementById('button'), 5, 5, {}); + } + + function* spec() { + let urls = [SAME_ORIGIN, CROSS_ORIGIN_1, CROSS_ORIGIN_2]; + for (let action of urls) { + for (let target of urls) { + yield { action: new URL(ACTION, action), + target: new URL(TARGET, target) }; + } + } + } + + info("Starting tests"); + let form = document.getElementById('form'); + + // The body of the POST needs to be large to trigger this. + // 1024*1024 seems to be enough, but scaling to get a margin. + document.getElementById('input').value = generateBody(1024*1024); + for (let { target, action } of spec()) { + add_task(async function runTest() { + info(`Running test ${target} with ${action}`); + form.action = action; + let frame = await withFrame(target); + await new Promise(resolve => { + addEventListener('message', async function() { + info("Form loaded"); + frame.remove(); + resolve(); + }, { once: true }); + + click(); + }); + + ok(true, `Submitted to ${origin} with target ${action}`) + }); + }; + </script> + </body> +</html> diff --git a/docshell/test/mochitest/test_bug1729662.html b/docshell/test/mochitest/test_bug1729662.html new file mode 100644 index 0000000000..ec43508494 --- /dev/null +++ b/docshell/test/mochitest/test_bug1729662.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test back/forward after pushState</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("Need to wait to make sure an event does not fire"); + + async function runTest() { + let win = window.open(); + let goneBackAndForwardOnce = new Promise((resolve) => { + let timeoutID; + + // We should only get one load event in win. + let bc = new BroadcastChannel("bug1729662"); + bc.addEventListener("message", () => { + bc.addEventListener("message", () => { + clearTimeout(timeoutID); + resolve(false); + }); + }, { once: true }); + + let goneBack = false, goneForward = false; + win.addEventListener("popstate", ({ state }) => { + // We should only go back and forward once, if we get another + // popstate after that then we should fall through to the + // failure case below. + if (!(goneBack && goneForward)) { + // Check if this is the popstate for the forward (the one for + // back will have state == undefined). + if (state == 1) { + ok(goneBack, "We should have gone back before going forward"); + + goneForward = true; + + // Wait a bit to make sure there are no more popstate events. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + timeoutID = setTimeout(resolve, 1000, true); + + return; + } + + // Check if we've gone back once before, if we get another + // popstate after that then we should fall through to the + // failure case below. + if (!goneBack) { + goneBack = true; + + return; + } + } + + clearTimeout(timeoutID); + resolve(false); + }); + }); + + win.location = "file_bug1729662.html"; + + ok(await goneBackAndForwardOnce, "Stopped navigating history"); + + win.close(); + + SimpleTest.finish(); + } + </script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1740516.html b/docshell/test/mochitest/test_bug1740516.html new file mode 100644 index 0000000000..b54932c736 --- /dev/null +++ b/docshell/test/mochitest/test_bug1740516.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test pageshow event order for iframe</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + function waitForPageShow(outer, inner) { + return new Promise((resolve) => { + let results = []; + outer.addEventListener("message", ({ data: persisted }) => { + results.push({ name: outer.name, persisted }); + if (results.length == 2) { + resolve(results); + } + }, { once: true }); + inner.addEventListener("message", ({ data: persisted }) => { + results.push({ name: inner.name, persisted }); + if (results.length == 2) { + resolve(results); + } + }, { once: true }); + }); + } + async function runTest() { + let outerBC = new BroadcastChannel("bug1740516_1"); + let innerBC = new BroadcastChannel("bug1740516_1_inner"); + + let check = waitForPageShow(outerBC, innerBC).then(([first, second]) => { + is(first.name, "bug1740516_1_inner", "Should get pageShow from inner iframe page first."); + ok(!first.persisted, "First navigation shouldn't come from BFCache."); + is(second.name, "bug1740516_1", "Should get pageShow from outer page second."); + ok(!second.persisted, "First navigation shouldn't come from BFCache."); + }, () => { + ok(false, "The promises should not be rejected."); + }); + window.open("file_bug1740516_1.html", "", "noopener"); + await check; + + check = waitForPageShow(outerBC, innerBC).then(([first, second]) => { + is(first.name, "bug1740516_1_inner", "Should get pageShow from inner iframe page first."); + ok(first.persisted, "Second navigation should come from BFCache"); + is(second.name, "bug1740516_1", "Should get pageShow from outer page second."); + ok(second.persisted, "Second navigation should come from BFCache"); + }, () => { + ok(false, "The promises should not be rejected."); + }); + outerBC.postMessage("navigate"); + await check; + + check = waitForPageShow(outerBC, innerBC).then(([first, second]) => { + is(first.name, "bug1740516_1_inner", "Should get pageShow from inner iframe page first."); + ok(!first.persisted, "Third navigation should not come from BFCache"); + is(second.name, "bug1740516_1", "Should get pageShow from outer page second."); + ok(!second.persisted, "Third navigation should not come from BFCache"); + }, () => { + ok(false, "The promises should not be rejected."); + }); + outerBC.postMessage("block_bfcache_and_navigate"); + await check; + + outerBC.postMessage("close"); + + outerBC.close(); + innerBC.close(); + + SimpleTest.finish(); + } + </script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1741132.html b/docshell/test/mochitest/test_bug1741132.html new file mode 100644 index 0000000000..1ae9727d9c --- /dev/null +++ b/docshell/test/mochitest/test_bug1741132.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test form restoration for no-store pages</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + // The number of entries which we keep in the BFCache (see nsSHistory.h). + const VIEWER_WINDOW = 3; + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + let bc = new BroadcastChannel("bug1741132"); + + // Setting the pref to 0 should evict all content viewers. + let load = SpecialPowers.pushPrefEnv({ + set: [["browser.sessionhistory.max_total_viewers", 0]], + }).then(() => { + // Set the pref to VIEWER_WINDOW + 2 now, to be sure we + // could fit all entries. + SpecialPowers.pushPrefEnv({ + set: [["browser.sessionhistory.max_total_viewers", VIEWER_WINDOW + 2]], + }); + }).then(() => { + return new Promise(resolve => { + bc.addEventListener("message", resolve, { once: true }); + window.open("file_bug1741132.html", "", "noopener"); + }); + }); + // We want to try to keep one entry too many in the BFCache, + // so we ensure that there's at least VIEWER_WINDOW + 2 + // entries in session history (with one for the displayed + // page). + for (let i = 0; i < VIEWER_WINDOW + 2; ++i) { + load = load.then(() => { + return new Promise((resolve) => { + bc.addEventListener("message", resolve, { once: true }); + bc.postMessage({ cmd: "load", arg: `file_bug1741132.html?${i}` }); + }); + }); + } + load.then(() => { + return new Promise((resolve) => { + bc.addEventListener("message", ({ data: persisted }) => { + resolve(persisted); + }, { once: true }); + // Go back past the first entry that should be in the BFCache. + bc.postMessage({ cmd: "go", arg: -(VIEWER_WINDOW + 1) }); + }); + }).then((persisted) => { + ok(!persisted, "Only 3 pages should be kept in the BFCache"); + }).then(() => { + return new Promise((resolve) => { + bc.addEventListener("message", ({ data: persisted }) => { + resolve(persisted); + }, { once: true }); + // Go forward to the first entry that should be in the BFCache. + bc.postMessage({ cmd: "go", arg: 1 }); + }); + }).then((persisted) => { + ok(persisted, "3 pages should be kept in the BFCache"); + + bc.postMessage("close"); + + bc.close(); + + SimpleTest.finish(); + }); + } + </script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1742865.html b/docshell/test/mochitest/test_bug1742865.html new file mode 100644 index 0000000000..c8f9a4eca3 --- /dev/null +++ b/docshell/test/mochitest/test_bug1742865.html @@ -0,0 +1,137 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Auto refreshing pages shouldn't add an entry to session history</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + const REFRESH_REDIRECT_TIMER = 15; + + // 2 tests (same and cross origin) consisting of 2 refreshes of maximum 1 seconds + // 2 tests (same and cross origin) consisting of 2 refreshes of REFRESH_REDIRECT_TIMER seconds + // => We need (2 * 1) + (2 * 15) seconds + SimpleTest.requestLongerTimeout(3); + SimpleTest.waitForExplicitFinish(); + + const SJS = new URL("file_bug1742865.sjs", location.href); + const SJS_OUTER = new URL("file_bug1742865_outer.sjs", location.href); + const SCROLL = 500; + + let tolerance; + function setup() { + return SpecialPowers.spawn(window.top, [], () => { + return SpecialPowers.getDOMWindowUtils(content.window).getResolution(); + }).then(resolution => { + // Allow a half pixel difference if the top document's resolution is lower + // than 1.0 because the scroll position is aligned with screen pixels + // instead of CSS pixels. + tolerance = resolution < 1.0 ? 0.5 : 0.0; + }); + } + + function checkScrollPosition(scrollPosition, shouldKeepScrollPosition) { + isfuzzy(scrollPosition, shouldKeepScrollPosition ? SCROLL : 0, tolerance, + `Scroll position ${shouldKeepScrollPosition ? "should" : "shouldn't"} be maintained for meta refresh`); + } + + function openWindowAndCheckRefresh(url, params, shouldAddToHistory, shouldKeepScrollPosition) { + info(`Running test for ${JSON.stringify(params)}`); + + url = new URL(url); + Object.entries(params).forEach(([k, v]) => { url.searchParams.append(k, v) }); + url.searchParams.append("scrollTo", SCROLL); + + let resetURL = new URL(SJS); + resetURL.search = "?reset"; + return fetch(resetURL).then(() => { + return new Promise((resolve) => { + let count = 0; + window.addEventListener("message", function listener({ data: { commandType, commandData = {} } }) { + if (commandType == "onChangedInputValue") { + let { historyLength, inputValue } = commandData; + + if (shouldAddToHistory) { + is(historyLength, count, "Auto-refresh should add entries to session history"); + } else { + is(historyLength, 1, "Auto-refresh shouldn't add entries to session history"); + } + + is(inputValue, "1234", "Input's value should have been changed"); + + win.postMessage("loadNext", "*"); + return; + } + + is(commandType, "pageShow", "Unknown command type"); + + let { inputValue, scrollPosition } = commandData; + + switch (++count) { + // file_bug1742865.sjs causes 3 loads: + // * first load, returns first meta refresh + // * second load, caused by first meta refresh, returns second meta refresh + // * third load, caused by second meta refresh, doesn't return a meta refresh + case 2: + checkScrollPosition(scrollPosition, shouldKeepScrollPosition); + break; + case 3: + checkScrollPosition(scrollPosition, shouldKeepScrollPosition); + win.postMessage("changeInputValue", "*"); + break; + case 4: + win.postMessage("back", "*"); + break; + case 5: + is(inputValue, "1234", "Entries for auto-refresh should be attached to session history"); + checkScrollPosition(scrollPosition, shouldKeepScrollPosition); + removeEventListener("message", listener); + win.close(); + resolve(); + break; + } + }); + let win = window.open(url); + }); + }); + } + + function doTest(seconds, crossOrigin, shouldAddToHistory, shouldKeepScrollPosition) { + let params = { + seconds, + crossOrigin, + }; + + return openWindowAndCheckRefresh(SJS, params, shouldAddToHistory, shouldKeepScrollPosition).then(() => + openWindowAndCheckRefresh(SJS_OUTER, params, shouldAddToHistory, shouldKeepScrollPosition) + ); + } + + async function runTest() { + const FAST = Math.min(1, REFRESH_REDIRECT_TIMER); + const SLOW = REFRESH_REDIRECT_TIMER + 1; + let tests = [ + // [ time, crossOrigin, shouldAddToHistory, shouldKeepScrollPosition ] + [ FAST, false, false, true ], + [ FAST, true, false, false ], + [ SLOW, false, false, true ], + [ SLOW, true, true, false ], + ]; + + await setup(); + + for (let [ time, crossOrigin, shouldAddToHistory, shouldKeepScrollPosition ] of tests) { + await doTest(time, crossOrigin, shouldAddToHistory, shouldKeepScrollPosition); + } + + SimpleTest.finish(); + } + </script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1743353.html b/docshell/test/mochitest/test_bug1743353.html new file mode 100644 index 0000000000..a5d88df3f6 --- /dev/null +++ b/docshell/test/mochitest/test_bug1743353.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test back/forward after pushState</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + function runTest() { + let bc = new BroadcastChannel("bug1743353"); + new Promise((resolve) => { + bc.addEventListener("message", () => { + resolve(); + }, { once: true }); + + window.open("file_bug1743353.html", "", "noopener"); + }).then(() => { + return new Promise(resolve => { + bc.addEventListener("message", () => { + resolve(); + }, { once: true }); + + bc.postMessage("load"); + }) + }).then(() => { + return new Promise(resolve => { + let results = []; + bc.addEventListener("message", function listener({ data }) { + results.push(data); + if (results.length == 3) { + bc.removeEventListener("message", listener); + resolve(results); + } + }); + + bc.postMessage("back"); + }); + }).then((results) => { + is(results[0], "pagehide", "First event should be 'pagehide'."); + is(results[1], "unload", "Second event should be 'unload'."); + is(results[2], "pageshow", "Third event should be 'pageshow'."); + + bc.postMessage("close"); + + SimpleTest.finish(); + }); + } + </script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1747033.html b/docshell/test/mochitest/test_bug1747033.html new file mode 100644 index 0000000000..539b78fec0 --- /dev/null +++ b/docshell/test/mochitest/test_bug1747033.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test history after loading multipart</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + function runTest() { + let bc = new BroadcastChannel("bug1747033"); + new Promise(resolve => { + bc.addEventListener("message", ({ data: { historyLength } }) => { + is(historyLength, 1, "Correct length for first normal load."); + + resolve(); + }, { once: true }); + + window.open("file_bug1747033.sjs", "", "noopener"); + }).then(() => { + return new Promise(resolve => { + let loaded = 0; + bc.addEventListener("message", function listener({ data: { historyLength } }) { + ++loaded; + + is(historyLength, 2, `Correct length for multipart load ${loaded}.`); + + // We want 3 parts in total. + if (loaded < 3) { + if (loaded == 2) { + // We've had 2 parts, make the server send the last part. + fetch("file_bug1747033.sjs?sendLastPart"); + } else { + fetch("file_bug1747033.sjs?sendNextPart"); + } + return; + } + + bc.removeEventListener("message", listener); + resolve(); + }); + + bc.postMessage({ cmd: "load", arg: "file_bug1747033.sjs?multipart" }); + }); + }).then(() => { + return new Promise(resolve => { + bc.addEventListener("message", ({ data: { historyLength } }) => { + is(historyLength, 2, "Correct length after calling replaceState in multipart."); + + resolve(); + }, { once: true }); + + bc.postMessage({ cmd: "replaceState", arg: "file_bug1747033.sjs?replaced" }); + }); + }).then(() => { + return new Promise(resolve => { + bc.addEventListener("message", ({ data: { historyLength } }) => { + is(historyLength, 3, "Correct length for first normal load after multipart."); + + resolve(); + }, { once: true }); + + bc.postMessage({ cmd: "load", arg: "file_bug1747033.sjs" }); + }); + }).then(() => { + return new Promise(resolve => { + let goneBack = 0; + bc.addEventListener("message", function listener({ data: { historyLength } }) { + ++goneBack; + + is(historyLength, 3, "Correct length after going back."); + + if (goneBack == 1) { + bc.postMessage({ cmd: "back" }); + } else if (goneBack == 2) { + bc.removeEventListener("message", listener); + resolve(); + } + }); + + bc.postMessage({ cmd: "back" }); + }); + }).then(() => { + bc.postMessage({ cmd: "close" }); + + SimpleTest.finish(); + }); + } + </script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug1773192.html b/docshell/test/mochitest/test_bug1773192.html new file mode 100644 index 0000000000..d4c42dc1a7 --- /dev/null +++ b/docshell/test/mochitest/test_bug1773192.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test referrer with going back</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + // file_bug1773192_1.html will send a message with some data on pageshow. + function waitForData(bc) { + return new Promise(resolve => { + bc.addEventListener( + "message", + ({ data }) => { + resolve(data); + }, + { once: true } + ); + }); + } + async function runTest() { + let bc = new BroadcastChannel("bug1743353"); + + let getData = waitForData(bc); + + window.open("file_bug1773192_1.html", "", "noreferrer"); + + await getData.then(({ referrer }) => { + is(referrer, "", "Referrer should be empty at first."); + }); + + getData = waitForData(bc); + + // When file_bug1773192_1.html receives this message it will navigate to + // file_bug1773192_2.html. file_bug1773192_2.html removes itself from + // history with replaceState and submits a form with the POST method to + // file_bug1773192_3.sjs. file_bug1773192_3.sjs goes back in history. + // We should end up back at file_bug1773192_1.html, which will send a + // message with some data on pageshow. + bc.postMessage("next"); + + await getData.then(({ location, referrer }) => { + let firstURL = new URL("file_bug1773192_1.html", location).toString(); + is(location, firstURL, "Location should be the first page again."); + is(referrer, firstURL, "Referrer should also be the first page."); + }); + + bc.postMessage("close"); + + SimpleTest.finish(); + } + </script> +</head> +<body onload="runTest();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug385434.html b/docshell/test/mochitest/test_bug385434.html new file mode 100644 index 0000000000..bc82de9daf --- /dev/null +++ b/docshell/test/mochitest/test_bug385434.html @@ -0,0 +1,211 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=385434 +--> +<head> + <title>Test for Bug 385434</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=385434">Mozilla Bug 385434</a> +<p id="display"></p> +<div id="content"> + <iframe id="frame" style="height:100px; width:100px; border:0"></iframe> + <div id="status" style="display: none"></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 385434 */ +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +SimpleTest.expectAssertions(0, 1); // bug 1333702 + +var gNumHashchanges = 0; +var gCallbackOnIframeLoad = false; +var gSampleEvent; + +function statusMsg(msg) { + var msgElem = document.createElement("p"); + msgElem.appendChild(document.createTextNode(msg)); + + document.getElementById("status").appendChild(msgElem); +} + +function longWait() { + setTimeout(function() { gGen.next(); }, 1000); +} + +// onIframeHashchange, onIframeLoad, and onIframeScroll are all called by the +// content we load into our iframe in order to notify the parent frame of an +// event which was fired. +function onIframeHashchange() { + gNumHashchanges++; + gGen.next(); +} + +function onIframeLoad() { + if (gCallbackOnIframeLoad) { + gCallbackOnIframeLoad = false; + gGen.next(); + } +} + +function onIframeScroll() { + is(gNumHashchanges, 0, "onscroll should fire before onhashchange."); +} + +function enableIframeLoadCallback() { + gCallbackOnIframeLoad = true; +} + +function noEventExpected(msg) { + is(gNumHashchanges, 0, msg); + + // Even if there's an error, set gNumHashchanges to 0 so other tests don't + // fail. + gNumHashchanges = 0; +} + +function eventExpected(msg) { + is(gNumHashchanges, 1, msg); + + // Eat up this event, whether the test above was true or not + gNumHashchanges = 0; +} + +/* + * The hashchange event is dispatched asynchronously, so if we want to observe + * it, we have to yield within run_test(), transferring control back to the + * event loop. + * + * When we're expecting our iframe to observe a hashchange event after we poke + * it, we just yield and wait for onIframeHashchange() to call gGen.next() and + * wake us up. + * + * When we're testing to ensure that the iframe doesn't dispatch a hashchange + * event, we try to hook onto the iframe's load event. We call + * enableIframeLoadCallback(), which causes onIframeLoad() to call gGen.next() + * upon the next observed load. After we get our callback, we check that a + * hashchange didn't occur. + * + * We can't always just wait for page load in order to observe that a + * hashchange didn't happen. In these cases, we call longWait() and yield + * until either a hashchange occurs or longWait's callback is scheduled. This + * is something of a hack; it's entirely possible that longWait won't wait long + * enough, and we won't observe what should have been a failure of the test. + * But it shouldn't happen that good code will randomly *fail* this test. + */ +function* run_test() { + /* + * TEST 1 tests that: + * <body onhashchange = ... > works, + * the event is (not) fired at the correct times + */ + var frame = document.getElementById("frame"); + var frameCw = frame.contentWindow; + + enableIframeLoadCallback(); + frameCw.document.location = "file_bug385434_1.html"; + // Wait for the iframe to load and for our callback to fire + yield undefined; + + noEventExpected("No hashchange expected initially."); + + sendMouseEvent({type: "click"}, "link1", frameCw); + yield undefined; + eventExpected("Clicking link1 should trigger a hashchange."); + + sendMouseEvent({type: "click"}, "link1", frameCw); + longWait(); + yield undefined; + // succeed if a hashchange event wasn't triggered while we were waiting + noEventExpected("Clicking link1 again should not trigger a hashchange."); + + sendMouseEvent({type: "click"}, "link2", frameCw); + yield undefined; + eventExpected("Clicking link2 should trigger a hashchange."); + + frameCw.history.go(-1); + yield undefined; + eventExpected("Going back should trigger a hashchange."); + + frameCw.history.go(1); + yield undefined; + eventExpected("Going forward should trigger a hashchange."); + + // window.location has a trailing '#' right now, so we append "link1", not + // "#link1". + frameCw.window.location = frameCw.window.location + "link1"; + yield undefined; + eventExpected("Assigning to window.location should trigger a hashchange."); + + // Set up history in the iframe which looks like: + // file_bug385434_1.html#link1 + // file_bug385434_2.html + // file_bug385434_1.html#foo <-- current page + enableIframeLoadCallback(); + frameCw.window.location = "file_bug385434_2.html"; + yield undefined; + + enableIframeLoadCallback(); + frameCw.window.location = "file_bug385434_1.html#foo"; + yield undefined; + + // Now when we do history.go(-2) on the frame, it *shouldn't* fire a + // hashchange. Although the URIs differ only by their hashes, they belong to + // two different Documents. + frameCw.history.go(-2); + longWait(); + yield undefined; + noEventExpected("Moving between different Documents shouldn't " + + "trigger a hashchange."); + + /* + * TEST 2 tests that: + * <frameset onhashchange = ... > works, + * the event is targeted at the window object + * the event's cancelable, bubbles settings are correct + */ + + enableIframeLoadCallback(); + frameCw.document.location = "file_bug385434_2.html"; + yield undefined; + + frameCw.document.location = "file_bug385434_2.html#foo"; + yield undefined; + + eventExpected("frame onhashchange should fire events."); + // iframe should set gSampleEvent + is(gSampleEvent.target, frameCw, + "The hashchange event should be targeted to the window."); + is(gSampleEvent.type, "hashchange", + "Event type should be 'hashchange'."); + is(gSampleEvent.cancelable, false, + "The hashchange event shouldn't be cancelable."); + is(gSampleEvent.bubbles, false, + "The hashchange event should not bubble."); + + /* + * TEST 3 tests that: + * hashchange is dispatched if the current document readyState is + * not "complete" (bug 504837). + */ + frameCw.document.location = "file_bug385434_3.html"; + yield undefined; + eventExpected("Hashchange should fire even if the document " + + "hasn't finished loading."); + + SimpleTest.finish(); +} + +var gGen = run_test(); +gGen.next(); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug387979.html b/docshell/test/mochitest/test_bug387979.html new file mode 100644 index 0000000000..0aca2ee89b --- /dev/null +++ b/docshell/test/mochitest/test_bug387979.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=387979 +--> +<head> + <title>Test for Bug 387979</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=387979">Mozilla Bug 387979</a> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 387979 */ +function a(s) { + var r; + try { r = frames[0].document.body; } catch (e) { r = e; } + is(r instanceof frames[0].HTMLBodyElement, true, "Can't get body" + s); +} +var p = 0; +function b() { + switch (++p) { + case 1: + frames[0].location = "about:blank"; + break; + case 2: + a("before reload"); + frames[0].location.reload(); + break; + case 3: + a("after reload"); + SimpleTest.finish(); + break; + } +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<p id="display"> + <iframe onload="b()"></iframe> + <pre id="p">-</pre> +</p> +</body> +</html> + diff --git a/docshell/test/mochitest/test_bug402210.html b/docshell/test/mochitest/test_bug402210.html new file mode 100644 index 0000000000..326f98cf9f --- /dev/null +++ b/docshell/test/mochitest/test_bug402210.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +While working on bug 402210, it came up that the code was doing + +a.href = proto + host + +which technically produces "https:host" instead of "https://host" and +that the code was relying on href's setting having fixup behaviour +for this kind of thing. + +If we rely on it, we might as well test for it, even if it isn't the +problem 402210 was meant to fix. + +https://bugzilla.mozilla.org/show_bug.cgi?id=402210 +--> +<head> + <title>Test for Bug 402210</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402210">Mozilla Bug 402210</a> +<p id="display"> + <a id="testlink">Test Link</a> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function runTest() { + $("testlink").href = "https:example.com"; + is($("testlink").href, "https://example.com/", "Setting href on an anchor tag should fixup missing slashes after https protocol"); + + $("testlink").href = "ftp:example.com"; + is($("testlink").href, "ftp://example.com/", "Setting href on an anchor tag should fixup missing slashes after non-http protocol"); + + SimpleTest.finish(); +} + +addLoadEvent(runTest); +</script> +</pre> +</body> +</html> + diff --git a/docshell/test/mochitest/test_bug404548.html b/docshell/test/mochitest/test_bug404548.html new file mode 100644 index 0000000000..a8a773dce5 --- /dev/null +++ b/docshell/test/mochitest/test_bug404548.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=404548 +--> +<head> + <title>Test for Bug 404548</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=404548">Mozilla Bug 404548</a> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 404548 */ +var firstRemoved = false; +var secondHidden = false; + +SimpleTest.waitForExplicitFinish(); + +var w = window.open("bug404548-subframe.html", "", "width=10,height=10"); + +function finishTest() { + is(firstRemoved, true, "Should have removed iframe from the DOM"); + is(secondHidden, true, "Should have fired pagehide on second kid"); + w.close(); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> + diff --git a/docshell/test/mochitest/test_bug413310.html b/docshell/test/mochitest/test_bug413310.html new file mode 100644 index 0000000000..4299605575 --- /dev/null +++ b/docshell/test/mochitest/test_bug413310.html @@ -0,0 +1,106 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=413310 +--> +<head> + <title>Test for Bug 413310</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=413310">Mozilla Bug 413310</a> +<p id="display"> +<script class="testbody" type="text/javascript"> + +if (navigator.platform.startsWith("Mac")) { + SimpleTest.expectAssertions(0, 2); +} else { + SimpleTest.expectAssertions(0, 1); +} + +/** Test for Bug 413310 */ + +// NOTE: If we ever make subframes do bfcache stuff, this test will need to be +// modified accordingly! It assumes that subframes do NOT get bfcached. +var onloadCount = 0; + +var step = -1; // One increment will come from the initial subframe onload. + // Note that this script should come before the subframe, + // so that doNextStep is defined when its onload handler fires. + +var textContent; + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(doNextStep); + +function doNextStep() { + ++step; + switch (step) { + case 1: + is(onloadCount, 1, "Loaded initial page"); + is($("i").contentWindow.location.href, + location.href.replace(/test_bug413310.html/, + "bug413310-subframe.html"), + "Unexpected subframe location after initial load"); + $("i").contentDocument.forms[0].submit(); + break; + case 2: + is(onloadCount, 2, "Loaded POST result"); + + is($("i").contentWindow.location.href, + location.href.replace(/test_bug413310.html/, + "bug413310-post.sjs"), + "Unexpected subframe location after POST load"); + + textContent = $("i").contentDocument.body.textContent; + isDeeply(textContent.match(/^POST /), ["POST "], "Not a POST?"); + + $("i").contentWindow.location.hash = "foo"; + setTimeout(doNextStep, 0); + break; + case 3: + is(onloadCount, 2, "Anchor scroll should not fire onload"); + is($("i").contentWindow.location.href, + location.href.replace(/test_bug413310.html/, + "bug413310-post.sjs#foo"), + "Unexpected subframe location after anchor scroll"); + is(textContent, $("i").contentDocument.body.textContent, + "Did a load when scrolling?"); + $("i").contentWindow.location.href = "bug413310-subframe.html"; + break; + case 4: + is(onloadCount, 3, "Done new load"); + is($("i").contentWindow.location.href, + location.href.replace(/test_bug413310.html/, + "bug413310-subframe.html"), + "Unexpected subframe location after new load"); + history.back(); + break; + case 5: + is(onloadCount, 4, + "History traversal didn't fire onload: bfcache issues!"); + is($("i").contentWindow.location.href, + location.href.replace(/test_bug413310.html/, + "bug413310-post.sjs#foo"), + "Unexpected subframe location"); + is(textContent, $("i").contentDocument.body.textContent, + "Did a load when going back?"); + SimpleTest.finish(); + break; + } +} +</script> +<!-- Use a timeout in onload so that we don't do a load immediately inside onload --> +<iframe id="i" src="bug413310-subframe.html" onload="setTimeout(doNextStep, 20)"> +</iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> + diff --git a/docshell/test/mochitest/test_bug475636.html b/docshell/test/mochitest/test_bug475636.html new file mode 100644 index 0000000000..fb1827ad04 --- /dev/null +++ b/docshell/test/mochitest/test_bug475636.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=475636 +Test that refresh to data: URIs don't inherit the principal +--> +<head> + <title>Test for Bug 475636</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="gen.next()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=475636">Mozilla Bug 475636</a> + +<div id="content" style="display: none"> + +</div> +<iframe id=loader></iframe> +<pre id="test"> +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var gen = runTests(); + +window.private = 42; + +window.addEventListener("message", function(e) { + gen.next(e.data); +}); + +var url = "file_bug475636.sjs?"; + +function* runTests() { + var loader = document.getElementById("loader"); + for (var testNum = 1; ; ++testNum) { + loader.src = url + testNum; + let res = (yield); + if (res == "done") { + SimpleTest.finish(); + return; + } + is(res, "pass"); + } +} + + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug509055.html b/docshell/test/mochitest/test_bug509055.html new file mode 100644 index 0000000000..57ede19b43 --- /dev/null +++ b/docshell/test/mochitest/test_bug509055.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=509055 +--> +<head> + <title>Test for Bug 509055</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=509055">Mozilla Bug 509055</a> +<p id="display"></p> +<div id="status"></div> +<div id="content"> +</div> +<pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 509055 */ + + SimpleTest.waitForExplicitFinish(); + + var gGen; + + function shortWait() { + setTimeout(function() { gGen.next(); }, 0, false); + } + + function onChildHashchange(e) { + // gGen might be undefined when we refresh the page, so we have to check here + dump("onChildHashchange() called.\n"); + if (gGen) + gGen.next(); + } + + function onChildLoad(e) { + if (gGen) + gGen.next(); + } + + async function* runTest() { + var popup = window.open("file_bug509055.html", "popup 0", + "height=200,width=200,location=yes," + + "menubar=yes,status=yes,toolbar=yes,dependent=yes"); + popup.hashchangeCallback = onChildHashchange; + popup.onload = onChildLoad; + dump("Waiting for initial load.\n"); + yield undefined; + + // Without this wait, the change to location.hash below doesn't create a + // SHEntry or enable the back button. + shortWait(); + dump("Got initial load. Spinning event loop.\n"); + yield undefined; + + popup.location.hash = "#1"; + dump("Waiting for hashchange.\n"); + yield undefined; + + popup.history.back(); + dump("Waiting for second hashchange.\n"); + yield undefined; // wait for hashchange + + popup.document.title = "Changed"; + + // Wait for listeners to be notified of the title change. + shortWait(); + dump("Got second hashchange. Spinning event loop.\n"); + yield undefined; + + let sheTitle = ""; + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + var sh = SpecialPowers.wrap(popup) + .docShell + .QueryInterface(SpecialPowers.Ci.nsIWebNavigation) + .sessionHistory; + + // Get the title of the inner popup's current SHEntry + sheTitle = sh.legacySHistory.getEntryAtIndex(sh.index).title; + } else { + let chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("getTitle", browsingContext => { + // eslint-disable-next-line no-shadow + let sh = browsingContext.sessionHistory; + let title = sh.getEntryAtIndex(sh.index).title; + sendAsyncMessage("title", title); + }); + }); + + let p = chromeScript.promiseOneMessage("title"); + let browsingContext = SpecialPowers.wrap(popup) + .docShell.browsingContext; + chromeScript.sendAsyncMessage("getTitle", browsingContext); + sheTitle = await p; + chromeScript.destroy(); + } + is(sheTitle, "Changed", "SHEntry's title should change when we change."); + + popup.close(); + + SimpleTest.executeSoon(SimpleTest.finish); + } + + window.addEventListener("load", function() { + gGen = runTest(); + gGen.next(); + }); + + </script> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug511449.html b/docshell/test/mochitest/test_bug511449.html new file mode 100644 index 0000000000..da95909d1c --- /dev/null +++ b/docshell/test/mochitest/test_bug511449.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=511449 +--> +<head> + <title>Test for Bug 511449</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/NativeKeyCodes.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=511449">Mozilla Bug 511449</a> +<p id="display"></p> +<div id="status"></div> +<div id="content"> +</div> +<input type="text" id="input"> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 511449 */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +window.addEventListener("load", runTest); + +var win = null; + +function runTest() { + document.getElementById("input").focus(); + win = window.open("file_bug511449.html", ""); + SimpleTest.waitForFocus(runNextTest, win); +} + +function runNextTest() { + var didClose = false; + win.onunload = function() { + didClose = true; + }; + synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, MAC_VK_ANSI_W, {metaKey: 1}, "w", "w"); + + setTimeout(function() { + ok(didClose, "Cmd+W should have closed the tab"); + if (!didClose) { + win.close(); + } + SimpleTest.finish(); + }, 1000); +} + +</script> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug529119-1.html b/docshell/test/mochitest/test_bug529119-1.html new file mode 100644 index 0000000000..1c89780fc7 --- /dev/null +++ b/docshell/test/mochitest/test_bug529119-1.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test bug 529119</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +var workingURL = "http://mochi.test:8888/tests/docshell/test/mochitest/bug529119-window.html"; +var faultyURL = "https://www.some-nonexistent-domain-27489274c892748217cn2384.test/"; + +var w = null; +var phase = 0; +var gotWrongPageOnTryAgainClick = false; +// Token that represents which page we currently have loaded. +var token = 0; + +function delay(msec) { + return new Promise(resolve => setTimeout(resolve, msec)); +} + +async function assignToken(tokenToAssign) { + await SpecialPowers.spawn(w, [tokenToAssign], + newToken => { this.content.token = newToken }); +} + +async function pollForPage(win) { + while (true) { + try { + // When we do our navigation, there may be an interstitial about:blank + // page if the navigation involves a process switch. That about:blank + // will exist between the new process's docshell being created and the + // actual page that's being loaded loading (which can happen async from + // the docshell creation). We want to avoid treating the initial + // about:blank as a new page. + // + // We could conceivably expose Document::IsInitialDocument() as a + // ChromeOnly thing and use it here, but let's just filter out all + // about:blank, since we don't expect any in this test. + var haveNewPage = await SpecialPowers.spawn(w, [token], + currentToken => this.content.token != currentToken && + this.content.location.href != "about:blank"); + + if (haveNewPage) { + ++token; + assignToken(token); + break; + } + } catch (e) { + // Something went wrong; just keep waiting. + } + + await delay(100); + } +} + +async function windowLoaded() { + switch (phase) { + case 0: + assignToken(token); + + /* 2. We have succeededfully loaded a page, now go to a faulty URL */ + window.setTimeout(function() { + w.location.href = faultyURL; + }, 0); + + phase = 1; + + await pollForPage(w); + is(await SpecialPowers.spawn(w, [], () => this.content.location.href), + faultyURL, + "Is on an error page initially"); + + /* 3. now, while we are on the error page, try to reload it, actually + click the "Try Again" button */ + SpecialPowers.spawn(w, [], () => this.content.location.reload()); + + await pollForPage(w); + + /* 4-finish, check we are still on the error page */ + is(await SpecialPowers.spawn(w, [], () => this.content.location.href), + faultyURL, + "Is on an error page"); + is(gotWrongPageOnTryAgainClick, false, + "Must not get www.example.com page on reload of an error page"); + w.close(); + SimpleTest.finish(); + break; + + case 1: + /* 4-check, we must not get here! */ + gotWrongPageOnTryAgainClick = true; + break; + } +} + +function startTest() { + /* 1. load a URL that leads to an error page */ + w = window.open(workingURL); +} + +</script> +</head> +<body onload="startTest();"> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug529119-2.html b/docshell/test/mochitest/test_bug529119-2.html new file mode 100644 index 0000000000..a8bd57d4f7 --- /dev/null +++ b/docshell/test/mochitest/test_bug529119-2.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test bug 529119</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +var workingURL = "http://mochi.test:8888/tests/docshell/test/mochitest/bug529119-window.html"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var faultyURL = "http://some-nonexistent-domain-27489274c892748217cn2384.test/"; + +var w = null; +var phase = 0; +var isWindowLoaded = false; +// Token that represents which page we currently have loaded. +var token = 0; + +function delay(msec) { + return new Promise(resolve => setTimeout(resolve, msec)); +} + +async function assignToken(tokenToAssign) { + await SpecialPowers.spawn(w, [tokenToAssign], + newToken => { this.content.token = newToken }); +} + +// Returns when a new page is loaded and returns whether that page is an +// error page. +async function pollForPage(win) { + while (true) { + try { + // When we do our navigation, there may be an interstitial about:blank + // page if the navigation involves a process switch. That about:blank + // will exist between the new process's docshell being created and the + // actual page that's being loaded loading (which can happen async from + // the docshell creation). We want to avoid treating the initial + // about:blank as a new page. + // + // We could conceivably expose Document::IsInitialDocument() as a + // ChromeOnly thing and use it here, but let's just filter out all + // about:blank, since we don't expect any in this test. + var haveNewPage = await SpecialPowers.spawn(w, [token], + currentToken => this.content.token != currentToken && + this.content.location.href != "about:blank"); + + if (haveNewPage) { + ++token; + assignToken(token); + + // In this test, error pages are non-same-origin with us, and non-error + // pages are same-origin. + let haveErrorPage = false; + try { + win.document.title; + } catch (ex) { + haveErrorPage = true; + } + return haveErrorPage; + } + } catch (e) { + // Something went wrong; just keep waiting. + } + + await delay(100); + } +} + +async function windowLoaded() { + // The code under here should only be run once + // The test popup window workingURL was already opened + if (isWindowLoaded) + return; + isWindowLoaded = true; + + assignToken(token); + + /* 2. We have successfully loaded a page, now go to a faulty URL */ + // XXX The test fails when we change the location synchronously + window.setTimeout(function() { + w.location.href = faultyURL; + }, 0); + + ok(await pollForPage(w), "Waiting for error page succeeded"); + /* 3. now, while we are on the error page, navigate back */ + try { + // We need the SpecialPowers bit, because this is a cross-origin window + // and we normally can't touch .history on those. + await SpecialPowers.spawn(w, [], () => this.content.history.back()); + } catch (ex) { + ok(false, "w.history.back() threw " + ex); + } + + ok(!await pollForPage(w), "Waiting for original page succeeded"); + /* 4-finish, check we are back at the original page */ + is(await SpecialPowers.spawn(w, [], () => this.content.location.href), + workingURL, + "Is on the previous page"); + w.close(); + SimpleTest.finish(); +} + +function startTest() { + /* 1. load a URL that leads to an error page */ + w = window.open(workingURL); +} + +</script> +</head> +<body onload="startTest();"> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug530396.html b/docshell/test/mochitest/test_bug530396.html new file mode 100644 index 0000000000..fa3ddc6db6 --- /dev/null +++ b/docshell/test/mochitest/test_bug530396.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=530396 +--> +<head> + <title>Test for Bug 530396</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=530396">Mozilla Bug 530396</a> + +<p> + +<iframe id="testFrame" src="http://mochi.test:8888/tests/docshell/test/mochitest/bug530396-subframe.html"></iframe> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// NOTE: If we ever make subframes do bfcache stuff, this test will need to be +// modified accordingly! It assumes that subframes do NOT get bfcached. +var onloadCount = 0; + +var step = 0; + +var gTestFrame = document.getElementById("testFrame"); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +addLoadEvent(doNextStep); + +function doNextStep() { + ++step; + switch (step) { + case 1: + is(onloadCount, 1, "Loaded initial page"); + sendMouseEvent({type: "click"}, "target2", gTestFrame.contentWindow); + window.setTimeout(doNextStep, 1000); + break; + + case 2: + is(onloadCount, 1, "opener must be null"); + sendMouseEvent({type: "click"}, "target1", gTestFrame.contentWindow); + break; + + case 3: + is(onloadCount, 2, "don't send referrer with rel=referrer"); + SimpleTest.finish(); + break; + } +} +</script> +</pre> +</html> diff --git a/docshell/test/mochitest/test_bug540462.html b/docshell/test/mochitest/test_bug540462.html new file mode 100644 index 0000000000..e0a0861aaf --- /dev/null +++ b/docshell/test/mochitest/test_bug540462.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=540462 +--> +<head> + <title>Test for Bug 540462</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=540462">Mozilla Bug 540462</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 540462 */ + +var win; +function runTest() { + win = window.open("file_bug540462.html", "", "width=100,height=100"); +} + +var dwlCount = 0; +var originalURL; +function documentWriteLoad() { + if (++dwlCount == 1) { + originalURL = win.document.body.firstChild.href; + } else if (dwlCount == 2) { + is(win.document.body.firstChild.href, originalURL, "Wrong href!"); + win.close(); + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug551225.html b/docshell/test/mochitest/test_bug551225.html new file mode 100644 index 0000000000..b2862fcad8 --- /dev/null +++ b/docshell/test/mochitest/test_bug551225.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=551225 +--> +<head> + <title>Test for Bug 551225</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=551225">Mozilla Bug 551225</a> + +<script type="application/javascript"> + +/** Test for Bug 551225 */ + +var obj = { + a: new Date("1/1/2000"), + b: /^foo$/, + c: "bar", +}; + +history.replaceState(obj, "", ""); +is(history.state.a.toString(), new Date("1/1/2000").toString(), "Date object."); +is(history.state.b.toString(), "/^foo$/", "Regex"); +is(history.state.c, "bar", "Other state"); + +</script> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug570341.html b/docshell/test/mochitest/test_bug570341.html new file mode 100644 index 0000000000..363f985407 --- /dev/null +++ b/docshell/test/mochitest/test_bug570341.html @@ -0,0 +1,142 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=570341 +--> +<head> + <title>Test for Bug 570341</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> + var start = Date.now(); + var moments = {}; + + var unload = 0; + var wasEnabled = true; + + function collectMoments() { + var win = frames[0]; + var timing = (win.performance && win.performance.timing) || {}; + for (let p in timing) { + moments[p] = timing[p]; + } + for (let p in win) { + if (p.substring(0, 9) == "_testing_") { + moments[p.substring(9)] = win[p]; + } + } + moments.evt_unload = unload; + return moments; + } + + function showSequence(node) { + while (node.firstChild) { + node.firstChild.remove(); + } + var sequence = []; + for (var p in moments) { + sequence.push(p); + } + sequence.sort(function(a, b) { + return moments[a] - moments[b]; + }); + var table = document.createElement("table"); + node.appendChild(table); + var row = document.createElement("tr"); + table.appendChild(row); + var cell = document.createElement("td"); + row.appendChild(cell); + cell.appendChild(document.createTextNode("start")); + cell = document.createElement("td"); + row.appendChild(cell); + cell.appendChild(document.createTextNode(start)); + for (var i = 0; i < sequence.length; ++i) { + var prop = sequence[i]; + row = document.createElement("tr"); + table.appendChild(row); + cell = document.createElement("td"); + row.appendChild(cell); + cell.appendChild(document.createTextNode(prop)); + cell = document.createElement("td"); + row.appendChild(cell); + cell.appendChild(document.createTextNode(moments[prop])); + } + } + + function checkValues() { + var win = frames[0]; + ok(win.performance, + "window.performance is missing or not accessible for frame"); + ok(!win.performance || win.performance.timing, + "window.performance.timing is missing or not accessible for frame"); + collectMoments(); + + var sequences = [ + ["navigationStart", "unloadEventStart", "unloadEventEnd"], + ["navigationStart", "fetchStart", "domainLookupStart", "domainLookupEnd", + "connectStart", "connectEnd", "requestStart", "responseStart", "responseEnd"], + ["responseStart", "domLoading", "domInteractive", "domComplete"], + ["domContentLoadedEventStart", "domContentLoadedEventEnd", + "loadEventStart", "loadEventEnd"], + ]; + + for (var i = 0; i < sequences.length; ++i) { + var seq = sequences[i]; + for (var j = 0; j < seq.length; ++j) { + var prop = seq[j]; + if (j > 0) { + var prevProp = seq[j - 1]; + ok(moments[prevProp] <= moments[prop], + ["Expected ", prevProp, " to happen before ", prop, + ", got ", prevProp, " = ", moments[prevProp], + ", ", prop, " = ", moments[prop]].join("")); + } + } + } + + SimpleTest.finish(); + } + +window.onload = function() { + var win = frames[0]; + win.addEventListener("unload", function() { + unload = Date.now(); + }, true); + var seenLoad = 0; + win.addEventListener("load", function() { + seenLoad = Date.now(); + }, true); + frames[0].location = "bug570341_recordevents.html"; + var interval = setInterval(function() { + // time constants here are arbitrary, chosen to allow the test to pass + var stopPolling = (win.performance && win.performance.loadEventEnd) || + (seenLoad && Date.now() >= seenLoad + 3000) || + Date.now() >= start + 30000; + if (stopPolling) { + clearInterval(interval); + checkValues(); + } else if (win._testing_evt_load) { + seenLoad = Date.now(); + } + }, 100); +}; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=570341">Mozilla Bug 570341</a> +<div id="frames"> +<iframe name="child0" src="navigation/blank.html"></iframe> +</div> +<button type="button" onclick="showSequence(document.getElementById('display'))"> + Show Events</button> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug580069.html b/docshell/test/mochitest/test_bug580069.html new file mode 100644 index 0000000000..bb0a3bc823 --- /dev/null +++ b/docshell/test/mochitest/test_bug580069.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=580069 +--> +<head> + <title>Test for Bug 580069</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=580069">Mozilla Bug 580069</a> + +<script type="application/javascript"> + +add_task(async function() { + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", "file_bug580069_1.html"); + + // Insert the initial <iframe> document, and wait for page1Load to be called + // after it loads. + document.body.appendChild(iframe); + await new Promise(resolve => { + window.page1Load = resolve; + }); + let iframeCw = iframe.contentWindow; + + info("iframe's location is: " + iframeCw.location + "\n"); + + // Submit the forum and wait for the initial page load using a POST load. + iframeCw.document.getElementById("form").submit(); + let method1 = await new Promise(resolve => { + window.page2Load = resolve; + }); + info("iframe's location is: " + iframeCw.location + ", method is " + method1 + "\n"); + is(method1, "POST", "Method for first load should be POST."); + + // Push a new state, and refresh the page. This refresh shouldn't pop up the + // "are you sure you want to refresh a page with POST data?" dialog. If it + // does, this test will hang and fail, and we'll see 'Refreshing iframe...' at + // the end of the test log. + iframeCw.history.replaceState("", "", "?replaced"); + + info("Refreshing iframe...\n"); + iframeCw.location.reload(); + let method2 = await new Promise(resolve => { + window.page2Load = resolve; + }); + + info("iframe's location is: " + iframeCw.location + ", method is " + method2 + "\n"); + is(method2, "GET", "Method for second load should be GET."); + is(iframeCw.location.search, "?replaced", "Wrong search on iframe after refresh."); +}); +</script> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug590573.html b/docshell/test/mochitest/test_bug590573.html new file mode 100644 index 0000000000..83554a7a66 --- /dev/null +++ b/docshell/test/mochitest/test_bug590573.html @@ -0,0 +1,198 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=590573 +--> +<head> + <title>Test for Bug 590573</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=590573">Mozilla Bug 590573</a> + +<script type='application/javascript'> +SimpleTest.waitForExplicitFinish(); + +// Listen to the first callback, since this indicates that the page loaded. +var page1LoadCallbackEnabled = true; +function page1Load() { + if (page1LoadCallbackEnabled) { + page1LoadCallbackEnabled = false; + dump("Got page1 load.\n"); + pageLoad(); + } else { + dump("Ignoring page1 load.\n"); + } +} + +var page1PageShowCallbackEnabled = false; +function page1PageShow() { + if (page1PageShowCallbackEnabled) { + page1PageShowCallbackEnabled = false; + dump("Got page1 pageshow.\n"); + pageLoad(); + } else { + dump("Ignoring page1 pageshow.\n"); + } +} + +var page2LoadCallbackEnabled = false; +function page2Load() { + if (page2LoadCallbackEnabled) { + page2LoadCallbackEnabled = false; + dump("Got page2 popstate.\n"); + pageLoad(); + } else { + dump("Ignoring page2 popstate.\n"); + } +} + +var page2PopstateCallbackEnabled = false; +function page2Popstate() { + if (page2PopstateCallbackEnabled) { + page2PopstateCallbackEnabled = false; + dump("Got page2 popstate.\n"); + pageLoad(); + } else { + dump("Ignoring page2 popstate.\n"); + } +} + +var page2PageShowCallbackEnabled = false; +function page2PageShow() { + if (page2PageShowCallbackEnabled) { + page2PageShowCallbackEnabled = false; + dump("Got page2 pageshow.\n"); + pageLoad(); + } else { + dump("Ignoring page2 pageshow.\n"); + } +} + +var popup = window.open("file_bug590573_1.html"); + +var gTestContinuation = null; +var loads = 0; +function pageLoad() { + loads++; + dump("pageLoad(loads=" + loads + ", page location=" + popup.location + ")\n"); + + if (!gTestContinuation) { + gTestContinuation = testBody(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + SimpleTest.finish(); + } +} + +function continueAsync() { + popup.addEventListener("popstate", function() { + popup.requestAnimationFrame(function() { gTestContinuation.next(); }); + }, + {once: true}); +} + +function* testBody() { + is(popup.scrollY, 0, "test 1"); + popup.scroll(0, 100); + + popup.history.pushState("", "", "?pushed"); + is(Math.round(popup.scrollY), 100, "test 2"); + popup.scroll(0, 200); // set state-2's position to 200 + + popup.history.back(); + continueAsync(); + yield; + is(Math.round(popup.scrollY), 100, "test 3"); + popup.scroll(0, 150); // set original page's position to 150 + + popup.history.forward(); + continueAsync(); + yield; + is(Math.round(popup.scrollY), 200, "test 4"); + + popup.history.back(); + continueAsync(); + yield; + is(Math.round(popup.scrollY), 150, "test 5"); + + popup.history.forward(); + continueAsync(); + yield; + is(Math.round(popup.scrollY), 200, "test 6"); + + // At this point, the history looks like: + // PATH POSITION + // file_bug590573_1.html 150 <-- oldest + // file_bug590573_1.html?pushed 200 <-- newest, current + + // Now test that the scroll position is persisted when we have real + // navigations involved. First, we need to spin the event loop so that the + // navigation doesn't replace our current history entry. + + setTimeout(pageLoad, 0); + yield; + + page2LoadCallbackEnabled = true; + popup.location = "file_bug590573_2.html"; + yield; + + ok(popup.location.href.match("file_bug590573_2.html$"), + "Location was " + popup.location + + " but should end with file_bug590573_2.html"); + + is(popup.scrollY, 0, "test 7"); + popup.scroll(0, 300); + + // We need to spin the event loop again before we go back, otherwise the + // scroll positions don't get updated properly. + setTimeout(pageLoad, 0); + yield; + + page1PageShowCallbackEnabled = true; + popup.history.back(); + yield; + + // Spin the event loop again so that we get the right scroll positions. + setTimeout(pageLoad, 0); + yield; + + is(popup.location.search, "?pushed"); + ok(popup.document.getElementById("div1"), "page should have div1."); + + is(Math.round(popup.scrollY), 200, "test 8"); + + popup.history.back(); + continueAsync(); + yield; + is(Math.round(popup.scrollY), 150, "test 9"); + popup.history.forward(); + continueAsync(); + yield; + + is(Math.round(popup.scrollY), 200, "test 10"); + + // Spin one last time... + setTimeout(pageLoad, 0); + yield; + + page2PageShowCallbackEnabled = true; + popup.history.forward(); + yield; + + // Bug 821821, on Android tegras we get 299 instead of 300 sometimes + const scrollY = Math.floor(popup.scrollY); + if (scrollY >= 299 && scrollY <= 300) { + is(1, 1, "test 11"); + } else { + is(1, 0, "test 11, got " + popup.scrollY + " for popup.scrollY instead of 299|300"); + } + popup.close(); +} +</script> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug598895.html b/docshell/test/mochitest/test_bug598895.html new file mode 100644 index 0000000000..e0b17e2663 --- /dev/null +++ b/docshell/test/mochitest/test_bug598895.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=598895 +--> +<head> + <title>Test for Bug 598895</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=598895">Mozilla Bug 598895</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 598895 */ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { +var win1 = window.open(); +win1.document.body.textContent = "Should show"; + +var windowsLoaded = 0; + +window.onmessage = async function(ev) { + is(ev.data, "loaded", "Message should be 'loaded'"); + if (++windowsLoaded == 2) { + var one = await snapshotWindow(win1); + var two = await snapshotWindow(win2); + var three = await snapshotWindow(win3); + win1.close(); + win2.close(); + win3.close(); + ok(compareSnapshots(one, two, true)[0], "Popups should look identical"); + ok(compareSnapshots(one, three, false)[0], "Popups should not look identical"); + + SimpleTest.finish(); + } +}; + +var win2 = window.open("file_bug598895_1.html"); +var win3 = window.open("file_bug598895_2.html"); +}); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug634834.html b/docshell/test/mochitest/test_bug634834.html new file mode 100644 index 0000000000..e1f87de000 --- /dev/null +++ b/docshell/test/mochitest/test_bug634834.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=634834 +--> +<head> + <title>Test for Bug 634834</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=634834">Mozilla Bug 634834</a> + +<script type='application/javascript'> +SimpleTest.waitForExplicitFinish(); + +function iframe_loaded() { + var loadedAfterPushstate = false; + $("iframe").onload = function() { + loadedAfterPushstate = true; + }; + + var obj = { name: "name" }; + obj.__defineGetter__("a", function() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + $("iframe").contentWindow.location = "http://example.com"; + + // Wait until we've loaded example.com. + do { + var r = new XMLHttpRequest(); + r.open("GET", location.href, false); + r.overrideMimeType("text/plain"); + try { r.send(null); } catch (e) {} + } while (!loadedAfterPushstate); + }); + + try { + $("iframe").contentWindow.history.pushState(obj, ""); + ok(false, "pushState should throw exception."); + } catch (e) { + ok(true, "pushState threw an exception."); + } + SimpleTest.finish(); +} + +</script> + +<iframe id='iframe' src='file_bug634834.html' onload='iframe_loaded()'></iframe> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug637644.html b/docshell/test/mochitest/test_bug637644.html new file mode 100644 index 0000000000..1e5f4380b4 --- /dev/null +++ b/docshell/test/mochitest/test_bug637644.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=637644 +--> +<head> + <title>Test for Bug 637644</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=637644">Mozilla Bug 637644</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 637644 */ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { +var win1 = window.open("", "", "height=500,width=500"); +win1.document.body.textContent = "Should show"; + +var windowsLoaded = 0; + +window.onmessage = async function(ev) { + is(ev.data, "loaded", "Message should be 'loaded'"); + if (++windowsLoaded == 2) { + var one = await snapshotWindow(win1); + var two = await snapshotWindow(win2); + var three = await snapshotWindow(win3); + win1.close(); + win2.close(); + win3.close(); + ok(compareSnapshots(one, two, true)[0], "Popups should look identical"); + ok(compareSnapshots(one, three, false)[0], "Popups should not look identical"); + + SimpleTest.finish(); + } +}; + +var win2 = window.open("file_bug637644_1.html", "", "height=500,width=500"); +var win3 = window.open("file_bug637644_2.html", "", "height=500,width=500"); +}); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug640387_1.html b/docshell/test/mochitest/test_bug640387_1.html new file mode 100644 index 0000000000..b8aab054a1 --- /dev/null +++ b/docshell/test/mochitest/test_bug640387_1.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=640387 +--> +<head> + <title>Test for Bug 640387</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=640387">Mozilla Bug 640387</a> + +<script type='application/javascript'> +SimpleTest.waitForExplicitFinish(); + +function* test() { + /* Spin the event loop so we get out of the onload handler. */ + SimpleTest.executeSoon(function() { gGen.next(); }); + yield undefined; + + popup.history.pushState("", "", "#hash1"); + popup.history.pushState("", "", "#hash2"); + + // Now the history looks like: + // file_bug640387.html + // file_bug640387.html#hash1 + // file_bug640387.html#hash2 <-- current + + // Going back should trigger a hashchange, which will wake us up from the + // yield. + popup.history.back(); + yield undefined; + ok(true, "Got first hashchange."); + + // Going back should wake us up again. + popup.history.back(); + yield undefined; + ok(true, "Got second hashchange."); + + // Now the history looks like: + // file_bug640387.html <-- current + // file_bug640387.html#hash1 + // file_bug640387.html#hash2 + + // Going forward should trigger a hashchange. + popup.history.forward(); + yield undefined; + ok(true, "Got third hashchange."); + + // Now modify the history so it looks like: + // file_bug640387.html + // file_bug640387.html#hash1 + // file_bug640387.html#hash1 <-- current + popup.history.pushState("", "", "#hash1"); + + // Now when we go back, we should not get a hashchange. Instead, wait for a + // popstate. We need to asynchronously go back because popstate is fired + // sync. + gHashchangeExpected = false; + gCallbackOnPopstate = true; + SimpleTest.executeSoon(function() { popup.history.back(); }); + yield undefined; + ok(true, "Got popstate."); + gCallbackOnPopstate = false; + + // Spin the event loop so hashchange has a chance to fire, if it's going to. + SimpleTest.executeSoon(function() { gGen.next(); }); + yield undefined; + + popup.close(); + SimpleTest.finish(); +} + +var gGen = null; +function childLoad() { + gGen = test(); + gGen.next(); +} + +var gHashchangeExpected = true; +function childHashchange() { + if (gHashchangeExpected) { + gGen.next(); + } else { + ok(false, "Got hashchange when we weren't expecting one."); + } +} + +var gCallbackOnPopstate = false; +function childPopstate() { + if (gCallbackOnPopstate) { + gGen.next(); + } +} + +/* We need to run this test in a popup, because navigating an iframe + * back/forwards tends to cause intermittent orange. */ +var popup = window.open("file_bug640387.html"); + +/* Control now flows up to childLoad(), called once the popup loads. */ + +</script> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug640387_2.html b/docshell/test/mochitest/test_bug640387_2.html new file mode 100644 index 0000000000..c248a64836 --- /dev/null +++ b/docshell/test/mochitest/test_bug640387_2.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=640387 +--> +<head> + <title>Test for Bug 640387</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=640387">Mozilla Bug 640387</a> + +<!-- Test that, when going from + + http://example.com/#foo + +to + + http://example.com/ + +via a non-history load, we do a true load, rather than a scroll. --> + +<script type='application/javascript'> +SimpleTest.waitForExplicitFinish(); + +var callbackOnLoad = false; +function childLoad() { + if (callbackOnLoad) { + callbackOnLoad = false; + gGen.next(); + } +} + +var errorOnHashchange = false; +var callbackOnHashchange = false; +function childHashchange() { + if (errorOnHashchange) { + ok(false, "Got unexpected hashchange."); + } + if (callbackOnHashchange) { + callbackOnHashchange = false; + gGen.next(); + } +} + +function* run_test() { + var iframe = $("iframe").contentWindow; + + ok(true, "Got first load"); + + // Spin the event loop so we exit the onload handler. + SimpleTest.executeSoon(function() { gGen.next(); }); + yield undefined; + + let origLocation = iframe.location + ""; + callbackOnHashchange = true; + iframe.location.hash = "#1"; + // Wait for a hashchange event. + yield undefined; + + ok(true, "Got hashchange."); + + iframe.location = origLocation; + // This should produce a load event and *not* a hashchange, because the + // result of the load is a different document than we had previously. + callbackOnLoad = true; + errorOnHashchange = true; + yield undefined; + + ok(true, "Got final load."); + + // Spin the event loop to give hashchange a chance to fire, if it's going to. + SimpleTest.executeSoon(function() { gGen.next(); }); + yield undefined; + + SimpleTest.finish(); +} + +callbackOnLoad = true; +var gGen = run_test(); + +</script> + +<iframe id='iframe' src='file_bug640387.html'></iframe> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug653741.html b/docshell/test/mochitest/test_bug653741.html new file mode 100644 index 0000000000..33ba7077e4 --- /dev/null +++ b/docshell/test/mochitest/test_bug653741.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=653741 +--> +<head> + <title>Test for Bug 653741</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=653741">Mozilla Bug 653741</a> + +<script type="application/javascript"> + +/** Test for Bug 653741 */ +SimpleTest.waitForExplicitFinish(); + +function childLoad() { + // Spin the event loop so we leave the onload handler. + SimpleTest.executeSoon(childLoad2); +} + +function childLoad2() { + let cw = $("iframe").contentWindow; + + // Save the Y offset. For sanity's sake, make sure it's not 0, because we + // should be at the bottom of the page! + let origYOffset = Math.round(cw.pageYOffset); + ok(origYOffset != 0, "Original Y offset is not 0."); + + // Scroll the iframe to the top, then navigate to #bottom again. + cw.scrollTo(0, 0); + + // Our current location is #bottom, so this should scroll us down to the + // bottom again. + cw.location = cw.location + ""; + + is(Math.round(cw.pageYOffset), origYOffset, "Correct offset after reloading page."); + SimpleTest.finish(); +} + +</script> + +<iframe height='100px' id='iframe' src='file_bug653741.html#bottom'></iframe> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug660404.html b/docshell/test/mochitest/test_bug660404.html new file mode 100644 index 0000000000..94e3f67aa1 --- /dev/null +++ b/docshell/test/mochitest/test_bug660404.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=660404 +--> +<head> + <title>Test for Bug 660404</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660404">Mozilla Bug 660404</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 660404 */ +SimpleTest.waitForExplicitFinish(); + +var textContent = +` + var bc = new BroadcastChannel("bug660404_multipart"); + bc.postMessage({command: "finishTest", + textContent: window.document.documentElement.textContent, + innerHTML: window.document.documentElement.innerHTML + }); + bc.close(); + window.close(); +`; +var innerHTML = +`<head><script> + var bc = new BroadcastChannel("bug660404_multipart"); + bc.postMessage({command: "finishTest", + textContent: window.document.documentElement.textContent, + innerHTML: window.document.documentElement.innerHTML + }); + bc.close(); + window.close(); +</` +// eslint-disable-next-line no-useless-concat ++ `script></head>` +; +var bc_multipart = new BroadcastChannel("bug660404_multipart"); +bc_multipart.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "finishTest") { + is(msg.textContent, textContent); + is(msg.innerHTML, innerHTML); + bc_multipart.close(); + SimpleTest.finish(); + } +} +var bc = new BroadcastChannel("bug660404"); +bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "pagehide") { + is(msg.persisted, true, "Should be bfcached when navigating to multipart"); + bc.close(); + } +} + +// If Fission is disabled, the pref is no-op. +SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + // Have to open a new window, since there's no bfcache in subframes + window.open("file_bug660404-1.html", "", "noopener"); +}); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug662170.html b/docshell/test/mochitest/test_bug662170.html new file mode 100644 index 0000000000..d25ee3bd0f --- /dev/null +++ b/docshell/test/mochitest/test_bug662170.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=662170 +--> +<head> + <title>Test for Bug 662170</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=662170">Mozilla Bug 662170</a> + +<script type="application/javascript"> + +/** Test for Bug 662170 */ +SimpleTest.waitForExplicitFinish(); + +function childLoad() { + // Spin the event loop so we leave the onload handler. + SimpleTest.executeSoon(childLoad2); +} + +function childLoad2() { + let cw = $("iframe").contentWindow; + + // When we initially load the page, we should be at the top. + is(cw.pageYOffset, 0, "Initial Y offset should be 0."); + + // Scroll the iframe to the bottom. + cw.scrollTo(0, 300); + + // Did we actually scroll somewhere? + isnot(Math.round(cw.pageYOffset), 0, "Y offset should be non-zero after scrolling."); + + // Now load file_bug662170.html#, which should take us to the top of the + // page. + cw.location = cw.location + "#"; + + is(cw.pageYOffset, 0, "Correct Y offset after loading #."); + SimpleTest.finish(); +} + +</script> + +<!-- When the iframe loads, it calls childLoad(). --> +<iframe height='100px' id='iframe' src='file_bug662170.html'></iframe> + +</body> +</html> diff --git a/docshell/test/mochitest/test_bug668513.html b/docshell/test/mochitest/test_bug668513.html new file mode 100644 index 0000000000..09c848b6c1 --- /dev/null +++ b/docshell/test/mochitest/test_bug668513.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=668513 +--> +<head> + <title>Test for Bug 668513</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668513">Mozilla Bug 668513</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +if (navigator.platform.startsWith("Linux")) { + SimpleTest.expectAssertions(0, 1); +} + +SimpleTest.waitForExplicitFinish(); +window.open("file_bug668513.html"); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug669671.html b/docshell/test/mochitest/test_bug669671.html new file mode 100644 index 0000000000..4470cd6682 --- /dev/null +++ b/docshell/test/mochitest/test_bug669671.html @@ -0,0 +1,145 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=669671 +--> +<head> + <title>Test for Bug 669671</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=669671">Mozilla Bug 669671</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 669671. + * + * This is a bit complicated. We have a script, file_bug669671.sjs, which counts + * how many times it's loaded and returns that count in the body of an HTML + * document. For brevity, call this page X. + * + * X is sent with Cache-Control: max-age=0 and can't be bfcached (it has an + * onunload handler). Our test does the following in a popup: + * + * 1) Load X?pushed, to prime the cache. + * 2) Navigate to X. + * 3) Call pushState and navigate from X to X?pushed. + * 4) Navigate to X?navigated. + * 5) Go back (to X?pushed). + * + * We do all this work so we can check that in step 5, we fetch X?pushed from + * the network -- we shouldn't use our cached copy, because of the + * cache-control header X sends. + * + * Then we go back and repeat the whole process but call history.replaceState + * instead of pushState. And for good measure, we test once more, this time + * modifying only the hash of the URI using replaceState. In this case, we + * should* load from the cache. + * + */ +SimpleTest.requestLongerTimeout(2); +SimpleTest.waitForExplicitFinish(); + +function onChildLoad() { + SimpleTest.executeSoon(function() { gGen.next(); }); +} + +var _loadCount = 0; +function checkPopupLoadCount() { + is(popup.document.body.innerHTML, _loadCount + "", "Load count"); + + // We normally want to increment _loadCount here. But if the test fails + // because we didn't do a load we should have, let's not cause a cascade of + // failures by incrementing _loadCount. + var origCount = _loadCount; + if (popup.document.body.innerHTML >= _loadCount + "") + _loadCount++; + return origCount; +} + +function* test() { + // Step 0 - Make sure the count is reset to 0 in case of reload + popup.location = "file_bug669671.sjs?countreset"; + yield; + is(popup.document.body.innerHTML, "0", + "Load count should be reset to 0"); + + // Step 1 - The popup's body counts how many times we've requested the + // resource. This is the first time we've requested it, so it should be '0'. + checkPopupLoadCount(); + + // Step 2 - We'll get another onChildLoad when this finishes. + popup.location = "file_bug669671.sjs"; + yield undefined; + + // Step 3 - Call pushState and change the URI back to ?pushed. + checkPopupLoadCount(); + popup.history.pushState("", "", "?pushed"); + + // Step 4 - Navigate away. This should trigger another onChildLoad. + popup.location = "file_bug669671.sjs?navigated-1"; + yield undefined; + + // Step 5 - Go back. This should result in another onload (because the file is + // not in bfcache) and should be the fourth time we've requested the sjs file. + checkPopupLoadCount(); + popup.history.back(); + yield undefined; + + // This is the check which was failing before we fixed the bug. + checkPopupLoadCount(); + + popup.close(); + + // Do the whole thing again, but with replaceState. + popup = window.open("file_bug669671.sjs?replaced"); + yield undefined; + checkPopupLoadCount(); + popup.location = "file_bug669671.sjs"; + yield undefined; + checkPopupLoadCount(); + popup.history.replaceState("", "", "?replaced"); + popup.location = "file_bug669671.sjs?navigated-2"; + yield undefined; + checkPopupLoadCount(); + popup.history.back(); + yield undefined; + checkPopupLoadCount(); + popup.close(); + + // Once more, with feeling. Notice that we don't have to prime the cache + // with an extra load here, because X and X#hash share the same cache entry. + popup = window.open("file_bug669671.sjs?hash-test"); + yield undefined; + var initialCount = checkPopupLoadCount(); + popup.history.replaceState("", "", "#hash"); + popup.location = "file_bug669671.sjs?navigated-3"; + yield undefined; + checkPopupLoadCount(); + popup.history.back(); + yield undefined; + is(popup.document.body.innerHTML, initialCount + "", + "Load count (should be cached)"); + popup.close(); + + SimpleTest.finish(); +} + +var gGen = test(); +var popup; + +// Disable RCWN to make cache behavior deterministic. +SpecialPowers.pushPrefEnv({set: [["network.http.rcwn.enabled", false]]}, () => { + // This will call into onChildLoad once it loads. + popup = window.open("file_bug669671.sjs?pushed"); +}); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug675587.html b/docshell/test/mochitest/test_bug675587.html new file mode 100644 index 0000000000..452bbc8058 --- /dev/null +++ b/docshell/test/mochitest/test_bug675587.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=675587 +--> +<head> + <title>Test for Bug 675587</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=675587">Mozilla Bug 675587</a> +<p id="display"> + <iframe src="file_bug675587.html#hash"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 675587 */ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + ok(window.frames[0].location.href.endsWith("file_bug675587.html#"), + "Should have the right href"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug680257.html b/docshell/test/mochitest/test_bug680257.html new file mode 100644 index 0000000000..4d5736ac0a --- /dev/null +++ b/docshell/test/mochitest/test_bug680257.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=680257 +--> +<head> + <title>Test for Bug 680257</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=680257">Mozilla Bug 680257</a> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var popup = window.open("file_bug680257.html"); + +var gTestContinuation = null; +function continueAsync() { + popup.addEventListener("hashchange", + function(e) { gTestContinuation.next(); }, { once: true }); +} + +// The popup will call into popupLoaded() once it loads. +function popupLoaded() { + // runTests() needs to be called from outside popupLoaded's onload handler. + // Otherwise, the navigations we do in runTests won't create new SHEntries. + SimpleTest.executeSoon(function() { + if (!gTestContinuation) { + gTestContinuation = runTests(); + } + gTestContinuation.next(); + }); +} + +function* runTests() { + checkPopupLinkStyle(false, "Initial"); + + popup.location.hash = "a"; + continueAsync(); + yield; + checkPopupLinkStyle(true, "After setting hash"); + + popup.history.back(); + continueAsync(); + yield; + + checkPopupLinkStyle(false, "After going back"); + + popup.history.forward(); + continueAsync(); + yield; + checkPopupLinkStyle(true, "After going forward"); + + popup.close(); + SimpleTest.finish(); +} + +function checkPopupLinkStyle(isTarget, desc) { + var link = popup.document.getElementById("a"); + var style = popup.getComputedStyle(link); + var color = style.getPropertyValue("color"); + + // Color is red if isTarget, black otherwise. + if (isTarget) { + is(color, "rgb(255, 0, 0)", desc); + } else { + is(color, "rgb(0, 0, 0)", desc); + } +} + +</script> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug691547.html b/docshell/test/mochitest/test_bug691547.html new file mode 100644 index 0000000000..706cd5013b --- /dev/null +++ b/docshell/test/mochitest/test_bug691547.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=691547 +--> +<head> + <title>Test for Bug 691547</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + var navStart = 0; + var beforeReload = 0; + function onContentLoad() { + var frame = frames[0]; + if (!navStart) { + // First time we perform navigation in subframe. The bug is that + // load in subframe causes timing.navigationStart to be recorded + // as if it was a start of the next navigation. + var innerFrame = frame.frames[0]; + navStart = frame.performance.timing.navigationStart; + innerFrame.location = "bug570341_recordevents.html"; + // Let's wait a bit so the difference is clear anough. + setTimeout(reload, 3000); + } else { + // Content reloaded, time to check. We are allowing a huge time slack, + // in case clock is imprecise. If we have a bug, the difference is + // expected to be about the timeout value set above. + var diff = frame.performance.timing.navigationStart - beforeReload; + ok(diff >= -200, + "navigationStart should be set after reload request. " + + "Measured difference: " + diff + " (should be positive)"); + SimpleTest.finish(); + } + } + function reload() { + var frame = frames[0]; + ok(navStart == frame.performance.timing.navigationStart, + "navigationStart should not change when frame loads."); + beforeReload = Date.now(); + frame.location.reload(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=570341">Mozilla Bug 570341</a> +<div id="frames"> +<iframe name="frame0" id="frame0" src="bug691547_frame.html" onload="onContentLoad()"></iframe> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug694612.html b/docshell/test/mochitest/test_bug694612.html new file mode 100644 index 0000000000..8ea4331c43 --- /dev/null +++ b/docshell/test/mochitest/test_bug694612.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=694612 +--> +<head> + <title>Test for Bug 694612</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=694612">Mozilla Bug 694612</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 694612 */ +SimpleTest.waitForExplicitFinish(); + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + ok(event.data.result, "should have performance API in an <object>"); + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} +</script> +<object type="text/html" + data="data:text/html,<script>parent.postMessage({result:performance!=null},'*');</script>"> +</object> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug703855.html b/docshell/test/mochitest/test_bug703855.html new file mode 100644 index 0000000000..4aa7b08800 --- /dev/null +++ b/docshell/test/mochitest/test_bug703855.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=703855 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 703855</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=703855">Mozilla Bug 703855</a> +<p id="display"></p> +<div id="content" style="display: none"> + <iframe id="f" src="file_bug703855.html"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 703855 */ + +SimpleTest.waitForExplicitFinish(); + +var timingAttributes = [ + "connectEnd", + "connectStart", + "domComplete", + "domContentLoadedEventEnd", + "domContentLoadedEventStart", + "domInteractive", + "domLoading", + "domainLookupEnd", + "domainLookupStart", + "fetchStart", + "loadEventEnd", + "loadEventStart", + "navigationStart", + "redirectEnd", + "redirectStart", + "requestStart", + "responseEnd", + "responseStart", + "unloadEventEnd", + "unloadEventStart", +]; +var originalTiming = {}; + +function runTest() { + var timing = $("f").contentWindow.performance.timing; + for (let i in timingAttributes) { + originalTiming[timingAttributes[i]] = timing[timingAttributes[i]]; + } + + var doc = $("f").contentDocument; + doc.open(); + doc.write("<!DOCTYPE html>"); + doc.close(); + + SimpleTest.executeSoon(function() { + var newTiming = $("f").contentWindow.performance.timing; + for (let i in timingAttributes) { + is(newTiming[timingAttributes[i]], originalTiming[timingAttributes[i]], + "document.open should not affect value of " + timingAttributes[i]); + } + SimpleTest.finish(); + }); +} + +addLoadEvent(function() { + SimpleTest.executeSoon(runTest); +}); + + + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug728939.html b/docshell/test/mochitest/test_bug728939.html new file mode 100644 index 0000000000..168184099a --- /dev/null +++ b/docshell/test/mochitest/test_bug728939.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=728939 +--> +<head> + <title>Test for Bug 728939</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=728939">Mozilla Bug 728939</a> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// Called when the popup finishes loading. +function popupLoaded() { + popup.location.hash = "#foo"; + is(popup.document.URL, popup.location.href, "After hashchange."); + + popup.history.pushState("", "", "bar"); + is(popup.document.URL, popup.location.href, "After pushState."); + + popup.history.replaceState("", "", "baz"); + is(popup.document.URL, popup.location.href, "After replaceState."); + + popup.close(); + SimpleTest.finish(); +} + +var popup = window.open("file_bug728939.html"); + +</script> +</body> +</html> diff --git a/docshell/test/mochitest/test_bug797909.html b/docshell/test/mochitest/test_bug797909.html new file mode 100644 index 0000000000..15b7c27c0f --- /dev/null +++ b/docshell/test/mochitest/test_bug797909.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=797909 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 797909</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=797909">Mozilla Bug 797909</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + /** Test for Bug 797909 */ + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + var iframe = document.getElementById("ifr"); + try { + iframe.contentWindow.document; + ok(false, "Should have thrown an exception"); + } catch (ex) { + ok(true, "Got an exception"); + } + + iframe = document.createElement("iframe"); + // set sandbox attribute + iframe.sandbox = "allow-scripts"; + // and then insert into the doc + document.body.appendChild(iframe); + + try { + iframe.contentWindow.document; + ok(false, "Should have thrown an exception"); + } catch (ex) { + ok(true, "Got an exception"); + } + + iframe = document.createElement("iframe"); + // set sandbox attribute + iframe.sandbox = "allow-same-origin"; + // and then insert into the doc + document.body.appendChild(iframe); + + try { + iframe.contentWindow.document; + ok(true, "Shouldn't have thrown an exception"); + } catch (ex) { + ok(false, "Got an unexpected exception"); + } + + SimpleTest.finish(); + } + +</script> +</pre> +<iframe id="ifr" sandbox = "allow-scripts"></iframe> +</body> +</html> diff --git a/docshell/test/mochitest/test_close_onpagehide_by_history_back.html b/docshell/test/mochitest/test_close_onpagehide_by_history_back.html new file mode 100644 index 0000000000..33140502f7 --- /dev/null +++ b/docshell/test/mochitest/test_close_onpagehide_by_history_back.html @@ -0,0 +1,24 @@ +<!doctype html> +<title>Test for closing window in pagehide event callback caused by history.back()</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432396">Mozilla Bug 1432396</a> +<p id="display"></p> +<script> +SimpleTest.waitForExplicitFinish(); + +const w = window.open("file_close_onpagehide1.html"); +window.addEventListener("message", e => { + is(e.data, "initial", "The initial page loaded"); + window.addEventListener("message", evt => { + is(evt.data, "second", "The second page loaded"); + w.onpagehide = () => { + w.close(); + info("try to close the popped up window in onpagehide"); + SimpleTest.finish(); + }; + w.history.back(); + }, { once: true }); + w.location = "file_close_onpagehide2.html"; +}, { once: true }); +</script> diff --git a/docshell/test/mochitest/test_close_onpagehide_by_window_close.html b/docshell/test/mochitest/test_close_onpagehide_by_window_close.html new file mode 100644 index 0000000000..8b094cdaa4 --- /dev/null +++ b/docshell/test/mochitest/test_close_onpagehide_by_window_close.html @@ -0,0 +1,20 @@ +<!doctype html> +<title>Test for closing window in pagehide event callback caused by window.close()</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432396">Mozilla Bug 1432396</a> +<p id="display"></p> +<script> +SimpleTest.waitForExplicitFinish(); + +const w = window.open("file_close_onpagehide1.html"); +window.addEventListener("message", e => { + is(e.data, "initial", "The initial page loaded"); + w.onpagehide = () => { + w.close(); + info("try to close the popped up window in onpagehide"); + SimpleTest.finish(); + }; + w.close(); +}, { once: true }); +</script> diff --git a/docshell/test/mochitest/test_compressed_multipart.html b/docshell/test/mochitest/test_compressed_multipart.html new file mode 100644 index 0000000000..438819b643 --- /dev/null +++ b/docshell/test/mochitest/test_compressed_multipart.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1600211 + +Loads a document that is served as multipart/x-mixed-replace as well as gzip compressed. +Checks that we correctly decompress and display it (via running JS within the document to notify us). +--> +<head> + <title>Test for Bug 1600211</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1600211">Mozilla Bug 1600211</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1600211 */ +SimpleTest.waitForExplicitFinish(); + +var w; + +function finishTest() { + is(w.document.documentElement.textContent, "opener.finishTest();"); + is(w.document.documentElement.innerHTML, "<head><script>opener.finishTest();</" + + "script></head>"); + w.close(); + SimpleTest.finish(); +} + +w = window.open("file_compressed_multipart"); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_content_javascript_loads.html b/docshell/test/mochitest/test_content_javascript_loads.html new file mode 100644 index 0000000000..eabc1d314e --- /dev/null +++ b/docshell/test/mochitest/test_content_javascript_loads.html @@ -0,0 +1,163 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for Bug 1647519</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1647519">Mozilla Bug 1647519</a> + +<script type="application/javascript"> +"use strict"; + +function promiseMessage(source, filter = event => true) { + return new Promise(resolve => { + function listener(event) { + if (event.source == source && filter(event)) { + window.removeEventListener("message", listener); + resolve(event); + } + } + window.addEventListener("message", listener); + }); +} + +async function runTests(resourcePath) { + /* globals Assert, content */ + let doc = content.document; + + // Sends a message to the given target window and waits for a response a few + // times to (more or less) ensure that a `javascript:` load request has had + // time to succeed, if it were going to. + async function doSomeRoundTrips(target) { + for (let i = 0; i < 3; i++) { + // Note: The ping message needs to be sent from a script running in the + // content scope or there will be no source window for the reply to be + // sent to. + await content.wrappedJSObject.ping(target); + } + } + + function promiseEvent(target, name) { + return new Promise(resolve => { + target.addEventListener(name, resolve, { once: true }); + }); + } + + function createIframe(host, id) { + let iframe = doc.createElement("iframe"); + iframe.id = id; + iframe.name = id; + iframe.src = `https://${host}${resourcePath}file_content_javascript_loads_frame.html`; + doc.body.appendChild(iframe); + return promiseEvent(iframe, "load"); + } + + const ID_SAME_ORIGIN = "frame-same-origin"; + const ID_SAME_BASE_DOMAIN = "frame-same-base-domain"; + const ID_CROSS_BASE_DOMAIN = "frame-cross-base-domain"; + + await Promise.all([ + createIframe("example.com", ID_SAME_ORIGIN), + createIframe("test1.example.com", ID_SAME_BASE_DOMAIN), + createIframe("example.org", ID_CROSS_BASE_DOMAIN), + ]); + + let gotJSLoadFrom = null; + let pendingJSLoadID = null; + content.addEventListener("message", event => { + if ("javascriptLoadID" in event.data) { + Assert.equal( + event.data.javascriptLoadID, + pendingJSLoadID, + "Message from javascript: load should have the expected ID" + ); + Assert.equal( + gotJSLoadFrom, + null, + "Should not have seen a previous load message this cycle" + ); + gotJSLoadFrom = event.source.name; + } + }); + + async function watchForJSLoads(frameName, expected, task) { + let loadId = Math.random(); + + let jsURI = + "javascript:" + + encodeURI(`parent.postMessage({ javascriptLoadID: ${loadId} }, "*")`); + + pendingJSLoadID = loadId; + gotJSLoadFrom = null; + + await task(jsURI); + + await doSomeRoundTrips(content.wrappedJSObject[frameName]); + + if (expected) { + Assert.equal( + gotJSLoadFrom, + frameName, + `Should have seen javascript: URI loaded into ${frameName}` + ); + } else { + Assert.equal( + gotJSLoadFrom, + null, + "Should not have seen javascript: URI loaded" + ); + } + } + + let frames = [ + { name: ID_SAME_ORIGIN, expectLoad: true }, + { name: ID_SAME_BASE_DOMAIN, expectLoad: false }, + { name: ID_CROSS_BASE_DOMAIN, expectLoad: false }, + ]; + for (let { name, expectLoad } of frames) { + info(`Checking loads for frame "${name}". Expecting loads: ${expectLoad}`); + + info("Checking location setter"); + await watchForJSLoads(name, expectLoad, jsURI => { + // Note: We need to do this from the content scope since security checks + // depend on the JS caller scope. + content.wrappedJSObject.setFrameLocation(name, jsURI); + }); + + info("Checking targeted <a> load"); + await watchForJSLoads(name, expectLoad, jsURI => { + let a = doc.createElement("a"); + a.target = name; + a.href = jsURI; + doc.body.appendChild(a); + a.click(); + a.remove(); + }); + + info("Checking targeted window.open load"); + await watchForJSLoads(name, expectLoad, jsURI => { + content.wrappedJSObject.open(jsURI, name); + }); + } +} + +add_task(async function() { + const resourcePath = location.pathname.replace(/[^\/]+$/, ""); + + let win = window.open( + `https://example.com${resourcePath}file_content_javascript_loads_root.html` + ); + await promiseMessage(win, event => event.data == "ready"); + + await SpecialPowers.spawn(win, [resourcePath], runTests); + + win.close(); +}); +</script> + +</body> +</html> diff --git a/docshell/test/mochitest/test_double_submit.html b/docshell/test/mochitest/test_double_submit.html new file mode 100644 index 0000000000..640930718d --- /dev/null +++ b/docshell/test/mochitest/test_double_submit.html @@ -0,0 +1,98 @@ +<!doctype html> +<html> + <head> + <title>Test for Bug 1590762</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <iframe name="targetFrame" id="targetFrame"></iframe> + <form id="form" action="double_submit.sjs?delay=1000" method="POST" target="targetFrame"> + <input id="token" type="text" name="token" value=""> + <input id="button" type="submit"> + </form> + <script> + "use strict"; + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const CROSS_ORIGIN_URI = "http://test1.example.com/tests/docshell/test/mochitest/ping.html"; + + function asyncClick(counts) { + let frame = document.createElement('iframe'); + frame.addEventListener( + 'load', () => frame.contentWindow.postMessage({command: "start"}, "*"), + { once:true }); + frame.src = "clicker.html"; + + addEventListener('message', ({source}) => { + if (source === frame.contentWindow) { + counts.click++; + synthesizeMouse(document.getElementById('button'), 5, 5, {}); + } + }, { once: true }); + + document.body.appendChild(frame); + return stop; + } + + function click(button) { + synthesizeMouse(button, 5, 5, {}); + } + + add_task(async function runTest() { + let frame = document.getElementById('targetFrame'); + await new Promise(resolve => { + addEventListener('message', resolve, {once: true}); + frame.src = CROSS_ORIGIN_URI; + }); + + let form = document.getElementById('form'); + let button = document.getElementById('button'); + + let token = document.getElementById('token'); + token.value = "first"; + + await new Promise((resolve, reject) => { + let counts = { click: 0, submit: 0 }; + form.addEventListener('submit', () => counts.submit++); + asyncClick(counts); + form.requestSubmit(button); + token.value = "bad"; + let steps = { + good: { + entered: false, + next: () => { steps.good.entered = true; resolve(); }, + assertion: () => { + ok(steps.first.entered && !steps.bad.entered, "good comes after first, but not bad") + } + }, + first: { + entered: false, + next: () => { steps.first.entered = true; token.value = "good"; click(button); }, + assertion: () => { + ok(!steps.good.entered && !steps.bad.entered, "first message is first") + is(counts.click, 1, "clicked"); + is(counts.submit, 2, "did submit"); + } + }, + bad: { + entered: false, + next: () => { reject(); }, + assertion: () => ok(false, "we got a bad message") + } + }; + addEventListener('message', ({source, data}) => { + if (source !== frame.contentWindow) { + return; + } + + let step = steps[data] || reject; + step.assertion(); + step.next(); + }) + }); + }); + </script> + </body> +</html> diff --git a/docshell/test/mochitest/test_forceinheritprincipal_overrule_owner.html b/docshell/test/mochitest/test_forceinheritprincipal_overrule_owner.html new file mode 100644 index 0000000000..70d610a677 --- /dev/null +++ b/docshell/test/mochitest/test_forceinheritprincipal_overrule_owner.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> + +var channel = SpecialPowers.wrap(window).docShell.currentDocumentChannel; +var loadInfo = channel.loadInfo; + +// 1) perform some sanity checks +var triggeringPrincipal = channel.loadInfo.triggeringPrincipal.asciiSpec; +var loadingPrincipal = channel.loadInfo.loadingPrincipal.asciiSpec; +var principalToInherit = channel.loadInfo.principalToInherit.asciiSpec; + +ok(triggeringPrincipal.startsWith("http://mochi.test:8888/") + || triggeringPrincipal.startsWith("http://mochi.xorigin-test:8888/"), + "initial triggeringPrincipal correct"); +ok(loadingPrincipal.startsWith("http://mochi.test:8888/") + || loadingPrincipal.startsWith("http://mochi.xorigin-test:8888/"), + "initial loadingPrincipal correct"); +ok(principalToInherit.startsWith("http://mochi.test:8888/") + || principalToInherit.startsWith("http://mochi.xorigin-test:8888/"), + "initial principalToInherit correct"); + +// reset principals on the loadinfo +loadInfo.resetPrincipalToInheritToNullPrincipal(); + +// 2) verify loadInfo contains the correct principals +triggeringPrincipal = channel.loadInfo.triggeringPrincipal.asciiSpec; +loadingPrincipal = channel.loadInfo.loadingPrincipal.asciiSpec; +principalToInherit = channel.loadInfo.principalToInherit; + +ok(triggeringPrincipal.startsWith("http://mochi.test:8888/") + || triggeringPrincipal.startsWith("http://mochi.xorigin-test:8888/"), + "triggeringPrincipal after resetting correct"); +ok(loadingPrincipal.startsWith("http://mochi.test:8888/") + || loadingPrincipal.startsWith("http://mochi.xorigin-test:8888/"), + "loadingPrincipal after resetting correct"); +ok(principalToInherit.isNullPrincipal + || principalToInherit.startsWith("http://mochi.xorigin-test:8888/"), + "principalToInherit after resetting correct"); + +// 3) verify that getChannelResultPrincipal returns right principal +var resultPrincipal = SpecialPowers.Services.scriptSecurityManager + .getChannelResultPrincipal(channel); + +ok(resultPrincipal.isNullPrincipal, + "resultPrincipal after resetting correct"); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_form_restoration.html b/docshell/test/mochitest/test_form_restoration.html new file mode 100644 index 0000000000..b929236770 --- /dev/null +++ b/docshell/test/mochitest/test_form_restoration.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test form restoration for no-store pages</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + function waitForMessage(aBroadcastChannel) { + return new Promise(resolve => { + aBroadcastChannel.addEventListener("message", ({ data }) => { + resolve(data); + }, { once: true }); + }); + } + + function postMessageAndWait(aBroadcastChannel, aMsg) { + let promise = waitForMessage(aBroadcastChannel); + aBroadcastChannel.postMessage(aMsg); + return promise; + } + + async function startTest(aTestFun) { + let bc = new BroadcastChannel("form_restoration"); + + let promise = waitForMessage(bc); + window.open("file_form_restoration_no_store.html", "", "noopener"); + await promise; + + // test steps + await aTestFun(bc); + + // close broadcast channel and window + bc.postMessage("close"); + bc.close(); + } + + /* Test for bug1740517 */ + add_task(async function history_back() { + await startTest(async (aBroadcastChannel) => { + // update form data + aBroadcastChannel.postMessage("enter_data"); + + // navigate + await postMessageAndWait(aBroadcastChannel, "navigate"); + + // history back + let { persisted, formData } = await postMessageAndWait(aBroadcastChannel, "back"); + + // check form data + ok(!persisted, "Page with a no-store header shouldn't be bfcached."); + is(formData, "initial", "We shouldn't restore form data when going back to a page with a no-store header."); + }); + }); + + /* Test for bug1752250 */ + add_task(async function location_reload() { + await startTest(async (aBroadcastChannel) => { + // update form data + aBroadcastChannel.postMessage("enter_data"); + + // reload + let { persisted, formData } = await postMessageAndWait(aBroadcastChannel, "reload"); + + // check form data + ok(!persisted, "Page with a no-store header shouldn't be bfcached."); + is(formData, "initial", "We shouldn't restore form data when reload a page with a no-store header."); + }); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_framedhistoryframes.html b/docshell/test/mochitest/test_framedhistoryframes.html new file mode 100644 index 0000000000..f90028af0a --- /dev/null +++ b/docshell/test/mochitest/test_framedhistoryframes.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=602256 +--> +<head> + <title>Test for Bug 602256</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602256">Mozilla Bug 602256</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 602256 */ + +SimpleTest.waitForExplicitFinish(); +var win = window.open("file_framedhistoryframes.html"); + +function done() { + win.close(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_iframe_srcdoc_to_remote.html b/docshell/test/mochitest/test_iframe_srcdoc_to_remote.html new file mode 100644 index 0000000000..05e0934d50 --- /dev/null +++ b/docshell/test/mochitest/test_iframe_srcdoc_to_remote.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<head> + <meta charset="utf-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> + +<body onload="test()"> + <script> + /* + Test to verify that when we change an OOP iframe to one that has a + srcdoc it loads in the correct process, which in this case is this + test document. + */ + SimpleTest.waitForExplicitFinish(); + async function test() { + // Create an OOP iframe + let frame = document.createElement("iframe"); + await new Promise(r => { + frame.onload = r; + document.body.appendChild(frame); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + frame.contentWindow.location = "http://example.net/tests/docshell/test/dummy_page.html"; + }); + if (SpecialPowers.useRemoteSubframes) { + ok(SpecialPowers.Cu.isRemoteProxy(frame.contentWindow), "should be a remote frame"); + } + + // Remove the attribute so we can set a srcdoc attribute on it + frame.removeAttribute("src"); + + // Set a srcdoc attribute on this iframe and wait for the load + await new Promise(r => { + frame.onload = r; + frame.setAttribute("srcdoc", '<html><body>body of the srcdoc frame</body></html>'); + }); + + // We should be in the same process as this test document + ok(!SpecialPowers.Cu.isRemoteProxy(frame.contentWindow), "should NOT be a remote frame"); + SimpleTest.finish(); + } + </script> +</body> + diff --git a/docshell/test/mochitest/test_javascript_sandboxed_popup.html b/docshell/test/mochitest/test_javascript_sandboxed_popup.html new file mode 100644 index 0000000000..edce93c26f --- /dev/null +++ b/docshell/test/mochitest/test_javascript_sandboxed_popup.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<head> +<meta charset="utf-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<iframe srcdoc="<a href='javascript:opener.parent.ok(false, `The JS ran!`)' target=_blank rel=opener>click</a>" + sandbox="allow-popups allow-same-origin"></iframe> + +<script> +add_task(async function() { + let promise = new Promise(resolve =>{ + SpecialPowers.addObserver(function obs(subject) { + is(subject.opener, window[0], + "blocked javascript URI should have been targeting the pop-up document"); + subject.close(); + SpecialPowers.removeObserver(obs, "javascript-uri-blocked-by-sandbox"); + resolve(); + }, "javascript-uri-blocked-by-sandbox"); + }); + document.querySelector("iframe").contentDocument.querySelector("a").click(); + await promise; +}); +</script> +</body> diff --git a/docshell/test/mochitest/test_load_during_reload.html b/docshell/test/mochitest/test_load_during_reload.html new file mode 100644 index 0000000000..88856b0100 --- /dev/null +++ b/docshell/test/mochitest/test_load_during_reload.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test loading a new page after calling reload()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + + function promiseForLoad() { + return new Promise(resolve => { + addEventListener("message", resolve, { once: true }); + }); + } + + add_task(async function runTest() { + let win = window.open("file_load_during_reload.html"); + await promiseForLoad(); + + win.location.reload(); + win.location.href = "file_load_during_reload.html?nextpage"; + await promiseForLoad(); + + ok(win.location.href.includes("nextpage"), "Should have loaded the next page."); + win.close(); + }); + + add_task(async function runTest2() { + let win = window.open("file_load_during_reload.html"); + await promiseForLoad(); + + win.history.replaceState("", "", "?1"); + win.location.reload(); + win.history.pushState("", "", "?2"); + win.location.reload(); + await promiseForLoad(); + + ok(win.location.href.includes("2"), "Should have loaded the second page."); + win.close(); + }); + + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_navigate_after_pagehide.html b/docshell/test/mochitest/test_navigate_after_pagehide.html new file mode 100644 index 0000000000..17d58d6e62 --- /dev/null +++ b/docshell/test/mochitest/test_navigate_after_pagehide.html @@ -0,0 +1,34 @@ +<!doctype html> +<html> + <head> + <title>Test for navigation attempts by scripts in inactive inner window</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> +<body> +<iframe src="dummy_page.html" id="iframe"></iframe> + +<script> +"use strict"; + +add_task(async function() { + let iframe = document.getElementById("iframe"); + + let navigate = iframe.contentWindow.eval(`(function() { + location.href = "/"; + })`); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + iframe.src = "http://example.com/"; + await new Promise(resolve => + iframe.addEventListener("load", resolve, { once: true }) + ); + + // This should do nothing. But, importantly, it should especially not crash. + navigate(); + + ok(true, "We didn't crash"); +}); +</script> +</body> +</html> diff --git a/docshell/test/mochitest/test_pushState_after_document_open.html b/docshell/test/mochitest/test_pushState_after_document_open.html new file mode 100644 index 0000000000..b81951b7ae --- /dev/null +++ b/docshell/test/mochitest/test_pushState_after_document_open.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=957479 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 957479</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 957479 */ + SimpleTest.waitForExplicitFinish(); + // Child needs to invoke us, otherwise our onload will fire before the child + // has done the write/close bit. + onmessage = function doTest() { + is(frames[0].location.pathname, "/tests/docshell/test/mochitest/file_pushState_after_document_open.html", + "Should have the right path here"); + is(frames[0].location.hash, "", "Should have the right hash here"); + frames[0].history.pushState({}, "", frames[0].document.URL + "#foopy"); + is(frames[0].location.pathname, "/tests/docshell/test/mochitest/file_pushState_after_document_open.html", + "Pathname should not have changed"); + is(frames[0].location.hash, "#foopy", "Hash should have changed"); + SimpleTest.finish(); + }; + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=957479">Mozilla Bug 957479</a> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe src="file_pushState_after_document_open.html"></iframe> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/test_redirect_history.html b/docshell/test/mochitest/test_redirect_history.html new file mode 100644 index 0000000000..a67c808405 --- /dev/null +++ b/docshell/test/mochitest/test_redirect_history.html @@ -0,0 +1,58 @@ +<!doctype html> +<html> + <head> + <title>Test for redirect from POST</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script> + "use strict"; + + info("Starting tests"); + + let tests = new Map([ + ["sameorigin", window.location.origin], + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + ["crossorigin", "http://test1.example.com"], + ]); + for (let [kind, origin] of tests) { + add_task(async function runTest() { + info(`Submitting to ${origin}`); + + let win; + await new Promise(resolve => { + addEventListener("message", resolve, { once: true }); + info("Loading file_redirect_history.html"); + win = window.open("file_redirect_history.html"); + }); + info("Done loading file_redirect_history.html"); + + let length = win.history.length; + let loc = win.location.toString(); + + await new Promise(resolve => { + addEventListener("message", resolve, { once: true }); + info("Posting"); + win.postMessage(`${origin}/tests/docshell/test/mochitest/form_submit_redirect.sjs?redirectTo=${loc}`, "*") + }); + info("Done posting\n"); + is(win.history.length, length, `Test ${kind}: history length should not change.`); + info(`Length=${win.history.length}`); + is(win.location.toString(), loc, `Test ${kind}: location should not change.`); + + await new Promise(resolve => { + addEventListener("message", resolve, { once: true }); + info("Reloading"); + win.location.reload(); + }); + info("Done reloading\n"); + is(win.location.toString(), loc, `Test ${kind}: location should not change after reload.`); + + win.close(); + }); + } + </script> + </body> +</html> diff --git a/docshell/test/mochitest/test_triggeringprincipal_location_seturi.html b/docshell/test/mochitest/test_triggeringprincipal_location_seturi.html new file mode 100644 index 0000000000..92e20b03ea --- /dev/null +++ b/docshell/test/mochitest/test_triggeringprincipal_location_seturi.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN_URI = "http://mochi.test:8888/tests/docshell/test/dummy_page.html"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const CROSS_ORIGIN_URI = "http://example.com/tests/docshell/test/dummy_page.html"; +const NUMBER_OF_TESTS = 3; +let testCounter = 0; + +function checkFinish() { + testCounter++; + if (testCounter < NUMBER_OF_TESTS) { + return; + } + SimpleTest.finish(); +} + +// ---- test 1 ---- + +let myFrame1 = document.createElement("iframe"); +myFrame1.src = SAME_ORIGIN_URI; +myFrame1.addEventListener("load", checkLoadFrame1); +document.documentElement.appendChild(myFrame1); + +function checkLoadFrame1() { + myFrame1.removeEventListener("load", checkLoadFrame1); + // window.location.href is no longer cross-origin accessible in gecko. + is(SpecialPowers.wrap(myFrame1.contentWindow).location.href, SAME_ORIGIN_URI, + "initial same origin dummy loaded into frame1"); + + SpecialPowers.wrap(myFrame1.contentWindow).location.hash = "#bar"; + is(SpecialPowers.wrap(myFrame1.contentWindow).location.href, SAME_ORIGIN_URI + "#bar", + "initial same origin dummy#bar loaded into iframe1"); + + myFrame1.addEventListener("load", checkNavFrame1); + myFrame1.src = CROSS_ORIGIN_URI; +} + +async function checkNavFrame1() { + myFrame1.removeEventListener("load", checkNavFrame1); + is(await SpecialPowers.spawn(myFrame1, [], () => this.content.location.href), + CROSS_ORIGIN_URI, + "cross origin dummy loaded into frame1"); + + myFrame1.addEventListener("load", checkBackNavFrame1); + myFrame1.src = SAME_ORIGIN_URI + "#bar"; +} + +async function checkBackNavFrame1() { + myFrame1.removeEventListener("load", checkBackNavFrame1); + is(await SpecialPowers.spawn(myFrame1, [], () => this.content.location.href), + SAME_ORIGIN_URI + "#bar", + "navagiating back to same origin dummy for frame1"); + checkFinish(); +} + +// ---- test 2 ---- + +let myFrame2 = document.createElement("iframe"); +myFrame2.src = "about:blank"; +myFrame2.addEventListener("load", checkLoadFrame2); +document.documentElement.appendChild(myFrame2); + +function checkLoadFrame2() { + myFrame2.removeEventListener("load", checkLoadFrame2); + is(SpecialPowers.wrap(myFrame2.contentWindow).location.href, "about:blank", + "initial about:blank frame loaded"); + + myFrame2.contentWindow.location.hash = "#foo"; + is(SpecialPowers.wrap(myFrame2.contentWindow).location.href, "about:blank#foo", + "about:blank#foo frame loaded"); + + myFrame2.addEventListener("load", checkHistoryFrame2); + myFrame2.src = "about:blank"; +} + +function checkHistoryFrame2() { + myFrame2.removeEventListener("load", checkHistoryFrame2); + is(SpecialPowers.wrap(myFrame2.contentWindow).location.href, "about:blank", + "about:blank frame loaded again"); + checkFinish(); +} + +// ---- test 3 ---- + +let myFrame3 = document.createElement("frame"); +document.documentElement.appendChild(myFrame3); +myFrame3.contentWindow.location.hash = "#foo"; + +is(myFrame3.contentWindow.location.href, "about:blank#foo", + "created history entry with about:blank#foo"); +checkFinish(); + +</script> +</body> +</html> diff --git a/docshell/test/mochitest/test_windowedhistoryframes.html b/docshell/test/mochitest/test_windowedhistoryframes.html new file mode 100644 index 0000000000..a874da098c --- /dev/null +++ b/docshell/test/mochitest/test_windowedhistoryframes.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=602256 +--> +<head> + <title>Test for Bug 602256</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602256">Mozilla Bug 602256</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 602256 */ + +SimpleTest.waitForExplicitFinish(); + +function done() { + subWin.close(); + SimpleTest.finish(); +} + +var subWin = window.open("historyframes.html", "_blank"); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/mochitest/url1_historyframe.html b/docshell/test/mochitest/url1_historyframe.html new file mode 100644 index 0000000000..b86af4b3fa --- /dev/null +++ b/docshell/test/mochitest/url1_historyframe.html @@ -0,0 +1 @@ +<p id='text'>Test1</p> diff --git a/docshell/test/mochitest/url2_historyframe.html b/docshell/test/mochitest/url2_historyframe.html new file mode 100644 index 0000000000..24374d1a5b --- /dev/null +++ b/docshell/test/mochitest/url2_historyframe.html @@ -0,0 +1 @@ +<p id='text'>Test2</p> diff --git a/docshell/test/moz.build b/docshell/test/moz.build new file mode 100644 index 0000000000..7cebe0339f --- /dev/null +++ b/docshell/test/moz.build @@ -0,0 +1,137 @@ +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Navigation") + +with Files("browser/*_bug234628*"): + BUG_COMPONENT = ("Core", "Internationalization") + +with Files("browser/*_bug349769*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("browser/*_bug388121*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("browser/*_bug655270*"): + BUG_COMPONENT = ("Toolkit", "Places") + +with Files("browser/*_bug655273*"): + BUG_COMPONENT = ("Firefox", "Menus") + +with Files("browser/*_bug852909*"): + BUG_COMPONENT = ("Firefox", "Menus") + +with Files("browser/*bug92473*"): + BUG_COMPONENT = ("Core", "Internationalization") + +with Files("browser/*loadDisallowInherit*"): + BUG_COMPONENT = ("Firefox", "Address Bar") + +with Files("browser/*tab_touch_events*"): + BUG_COMPONENT = ("Core", "DOM: Events") + +with Files("browser/*timelineMarkers*"): + BUG_COMPONENT = ("DevTools", "Performance Tools (Profiler/Timeline)") + +with Files("browser/*ua_emulation*"): + BUG_COMPONENT = ("DevTools", "General") + +with Files("chrome/*112564*"): + BUG_COMPONENT = ("Core", "Networking: HTTP") + +with Files("chrome/*303267*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("chrome/*453650*"): + BUG_COMPONENT = ("Core", "Layout") + +with Files("chrome/*565388*"): + BUG_COMPONENT = ("Core", "Widget") + +with Files("chrome/*582176*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("chrome/*608669*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("chrome/*690056*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("chrome/*92598*"): + BUG_COMPONENT = ("Core", "Networking: HTTP") + +with Files("iframesandbox/**"): + BUG_COMPONENT = ("Core", "Security") + +with Files("iframesandbox/*marquee_event_handlers*"): + BUG_COMPONENT = ("Core", "DOM: Security") + + +with Files("mochitest/*1045096*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("mochitest/*1151421*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("mochitest/*402210*"): + BUG_COMPONENT = ("Core", "DOM: Security") + +with Files("mochitest/*509055*"): + BUG_COMPONENT = ("Firefox", "Bookmarks & History") + +with Files("mochitest/*511449*"): + BUG_COMPONENT = ("Core", "Widget: Cocoa") + +with Files("mochitest/*551225*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("mochitest/*570341*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("mochitest/*580069*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("mochitest/*637644*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("mochitest/*640387*"): + BUG_COMPONENT = ("Core", "DOM: Events") + +with Files("mochitest/*668513*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("mochitest/*797909*"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +with Files("mochitest/*forceinheritprincipal*"): + BUG_COMPONENT = ("Core", "DOM: Security") + + +with Files("navigation/*13871.html"): + BUG_COMPONENT = ("Core", "Security") + +with Files("navigation/*386782*"): + BUG_COMPONENT = ("Core", "DOM: Editor") + +with Files("navigation/*430624*"): + BUG_COMPONENT = ("Core", "DOM: Editor") + +with Files("navigation/*430723*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("navigation/*child*"): + BUG_COMPONENT = ("Core", "Security") + +with Files("navigation/*opener*"): + BUG_COMPONENT = ("Core", "Security") + +with Files("navigation/*reserved*"): + BUG_COMPONENT = ("Core", "Security") + +with Files("navigation/*triggering*"): + BUG_COMPONENT = ("Core", "DOM: Security") + + +with Files("unit/*442584*"): + BUG_COMPONENT = ("Core", "Networking: Cache") + +with Files("unit/*setUsePrivateBrowsing*"): + BUG_COMPONENT = ("Firefox", "Extension Compatibility") diff --git a/docshell/test/navigation/NavigationUtils.js b/docshell/test/navigation/NavigationUtils.js new file mode 100644 index 0000000000..c4b52dc62f --- /dev/null +++ b/docshell/test/navigation/NavigationUtils.js @@ -0,0 +1,203 @@ +/* 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/. */ + +// ///////////////////////////////////////////////////////////////////////// +// +// Utilities for navigation tests +// +// ///////////////////////////////////////////////////////////////////////// + +var body = "This frame was navigated."; +var target_url = "navigation_target_url.html"; + +var popup_body = "This is a popup"; +var target_popup_url = "navigation_target_popup_url.html"; + +// ///////////////////////////////////////////////////////////////////////// +// Functions that navigate frames +// ///////////////////////////////////////////////////////////////////////// + +function navigateByLocation(wnd) { + try { + wnd.location = target_url; + } catch (ex) { + // We need to keep our finished frames count consistent. + // Oddly, this ends up simulating the behavior of IE7. + window.open(target_url, "_blank", "width=10,height=10"); + } +} + +function navigateByOpen(name) { + window.open(target_url, name, "width=10,height=10"); +} + +function navigateByForm(name) { + var form = document.createElement("form"); + form.action = target_url; + form.method = "POST"; + form.target = name; + document.body.appendChild(form); + form.submit(); +} + +var hyperlink_count = 0; + +function navigateByHyperlink(name) { + var link = document.createElement("a"); + link.href = target_url; + link.target = name; + link.id = "navigation_hyperlink_" + hyperlink_count++; + document.body.appendChild(link); + sendMouseEvent({ type: "click" }, link.id); +} + +// ///////////////////////////////////////////////////////////////////////// +// Functions that call into Mochitest framework +// ///////////////////////////////////////////////////////////////////////// + +async function isNavigated(wnd, message) { + var result = null; + try { + result = await SpecialPowers.spawn(wnd, [], () => + this.content.document.body.innerHTML.trim() + ); + } catch (ex) { + result = ex; + } + is(result, body, message); +} + +function isBlank(wnd, message) { + var result = null; + try { + result = wnd.document.body.innerHTML.trim(); + } catch (ex) { + result = ex; + } + is(result, "This is a blank document.", message); +} + +function isAccessible(wnd, message) { + try { + wnd.document.body.innerHTML; + ok(true, message); + } catch (ex) { + ok(false, message); + } +} + +function isInaccessible(wnd, message) { + try { + wnd.document.body.innerHTML; + ok(false, message); + } catch (ex) { + ok(true, message); + } +} + +function delay(msec) { + return new Promise(resolve => setTimeout(resolve, msec)); +} + +// ///////////////////////////////////////////////////////////////////////// +// Functions that uses SpecialPowers.spawn +// ///////////////////////////////////////////////////////////////////////// + +async function waitForFinishedFrames(numFrames) { + SimpleTest.requestFlakyTimeout("Polling"); + + var finishedWindows = new Set(); + + async function searchForFinishedFrames(win) { + try { + let { href, bodyText, readyState } = await SpecialPowers.spawn( + win, + [], + () => { + return { + href: this.content.location.href, + bodyText: + this.content.document.body && + this.content.document.body.textContent.trim(), + readyState: this.content.document.readyState, + }; + } + ); + + if ( + (href.endsWith(target_url) || href.endsWith(target_popup_url)) && + (bodyText == body || bodyText == popup_body) && + readyState == "complete" + ) { + finishedWindows.add(SpecialPowers.getBrowsingContextID(win)); + } + } catch (e) { + // This may throw if a frame is not fully initialized, in which + // case we'll handle it in a later iteration. + } + + for (let i = 0; i < win.frames.length; i++) { + await searchForFinishedFrames(win.frames[i]); + } + } + + while (finishedWindows.size < numFrames) { + await delay(500); + + for (let win of SpecialPowers.getGroupTopLevelWindows(window)) { + win = SpecialPowers.unwrap(win); + await searchForFinishedFrames(win); + } + } + + if (finishedWindows.size > numFrames) { + throw new Error("Too many frames loaded."); + } +} + +async function getFramesByName(name) { + let results = []; + for (let win of SpecialPowers.getGroupTopLevelWindows(window)) { + win = SpecialPowers.unwrap(win); + if ( + (await SpecialPowers.spawn(win, [], () => this.content.name)) === name + ) { + results.push(win); + } + } + + return results; +} + +async function cleanupWindows() { + for (let win of SpecialPowers.getGroupTopLevelWindows(window)) { + win = SpecialPowers.unwrap(win); + if (win.closed) { + continue; + } + + let href = ""; + try { + href = await SpecialPowers.spawn( + win, + [], + () => + this.content && this.content.location && this.content.location.href + ); + } catch (error) { + // SpecialPowers.spawn(win, ...) throws if win is closed. We did + // our best to not call it on a closed window, but races happen. + if (!win.closed) { + throw error; + } + } + + if ( + href && + (href.endsWith(target_url) || href.endsWith(target_popup_url)) + ) { + win.close(); + } + } +} diff --git a/docshell/test/navigation/blank.html b/docshell/test/navigation/blank.html new file mode 100644 index 0000000000..5360333f1d --- /dev/null +++ b/docshell/test/navigation/blank.html @@ -0,0 +1 @@ +<html><body>This is a blank document.</body></html>
\ No newline at end of file diff --git a/docshell/test/navigation/bluebox_bug430723.html b/docshell/test/navigation/bluebox_bug430723.html new file mode 100644 index 0000000000..5dcc533562 --- /dev/null +++ b/docshell/test/navigation/bluebox_bug430723.html @@ -0,0 +1,6 @@ +<html><head> +<script> window.addEventListener("pageshow", function() { opener.nextTest(); }); </script> +</head><body> +<div style="position:absolute; left:0px; top:0px; width:50%; height:150%; background-color:blue"> +<p>This is a very tall blue box.</p> +</div></body></html> diff --git a/docshell/test/navigation/browser.ini b/docshell/test/navigation/browser.ini new file mode 100644 index 0000000000..baca6b401c --- /dev/null +++ b/docshell/test/navigation/browser.ini @@ -0,0 +1,23 @@ +[DEFAULT] +support-files = + bug343515_pg1.html + bug343515_pg2.html + bug343515_pg3.html + bug343515_pg3_1.html + bug343515_pg3_1_1.html + bug343515_pg3_2.html + blank.html + redirect_to_blank.sjs + +[browser_bug1757458.js] +[browser_test_bfcache_eviction.js] +[browser_bug343515.js] +[browser_test-content-chromeflags.js] +tags = openwindow +[browser_ghistorymaxsize_is_0.js] +[browser_test_shentry_wireframe.js] +skip-if = + !sessionHistoryInParent + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_test_simultaneous_normal_and_history_loads.js] +skip-if = !sessionHistoryInParent # The test is for the new session history diff --git a/docshell/test/navigation/browser_bug1757458.js b/docshell/test/navigation/browser_bug1757458.js new file mode 100644 index 0000000000..cc9504ac1c --- /dev/null +++ b/docshell/test/navigation/browser_bug1757458.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + let testPage = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "view-source:http://example.com" + ) + "redirect_to_blank.sjs"; + + let testPage2 = "data:text/html,<div>Second page</div>"; + await BrowserTestUtils.withNewTab( + { gBrowser, url: testPage }, + async function (browser) { + await ContentTask.spawn(browser, [], async () => { + Assert.ok( + content.document.getElementById("viewsource").localName == "body", + "view-source document's body should have id='viewsource'." + ); + content.document + .getElementById("viewsource") + .setAttribute("onunload", "/* disable bfcache*/"); + }); + + BrowserTestUtils.loadURIString(browser, testPage2); + await BrowserTestUtils.browserLoaded(browser); + + let pageShownPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow", + true + ); + browser.browsingContext.goBack(); + await pageShownPromise; + + await ContentTask.spawn(browser, [], async () => { + Assert.ok( + content.document.getElementById("viewsource").localName == "body", + "view-source document's body should have id='viewsource'." + ); + }); + } + ); +}); diff --git a/docshell/test/navigation/browser_bug343515.js b/docshell/test/navigation/browser_bug343515.js new file mode 100644 index 0000000000..722b1bc018 --- /dev/null +++ b/docshell/test/navigation/browser_bug343515.js @@ -0,0 +1,276 @@ +// Test for bug 343515 - Need API for tabbrowsers to tell docshells they're visible/hidden + +// Globals +var testPath = "http://mochi.test:8888/browser/docshell/test/navigation/"; +var ctx = {}; + +add_task(async function () { + // Step 1. + + // Get a handle on the initial tab + ctx.tab0 = gBrowser.selectedTab; + ctx.tab0Browser = gBrowser.getBrowserForTab(ctx.tab0); + + await BrowserTestUtils.waitForCondition( + () => ctx.tab0Browser.docShellIsActive, + "Timed out waiting for initial tab to be active." + ); + + // Open a New Tab + ctx.tab1 = BrowserTestUtils.addTab(gBrowser, testPath + "bug343515_pg1.html"); + ctx.tab1Browser = gBrowser.getBrowserForTab(ctx.tab1); + await BrowserTestUtils.browserLoaded(ctx.tab1Browser); + + // Step 2. + is( + testPath + "bug343515_pg1.html", + ctx.tab1Browser.currentURI.spec, + "Got expected tab 1 url in step 2" + ); + + // Our current tab should still be active + ok(ctx.tab0Browser.docShellIsActive, "Tab 0 should still be active"); + ok(!ctx.tab1Browser.docShellIsActive, "Tab 1 should not be active"); + + // Switch to tab 1 + await BrowserTestUtils.switchTab(gBrowser, ctx.tab1); + + // Tab 1 should now be active + ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive"); + ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active"); + + // Open another tab + ctx.tab2 = BrowserTestUtils.addTab(gBrowser, testPath + "bug343515_pg2.html"); + ctx.tab2Browser = gBrowser.getBrowserForTab(ctx.tab2); + + await BrowserTestUtils.browserLoaded(ctx.tab2Browser); + + // Step 3. + is( + testPath + "bug343515_pg2.html", + ctx.tab2Browser.currentURI.spec, + "Got expected tab 2 url in step 3" + ); + + // Tab 0 should be inactive, Tab 1 should be active + ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive"); + ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active"); + + // Tab 2's window _and_ its iframes should be inactive + ok(!ctx.tab2Browser.docShellIsActive, "Tab 2 should be inactive"); + + await SpecialPowers.spawn(ctx.tab2Browser, [], async function () { + Assert.equal(content.frames.length, 2, "Tab 2 should have 2 iframes"); + for (var i = 0; i < content.frames.length; i++) { + info("step 3, frame " + i + " info: " + content.frames[i].location); + let bc = content.frames[i].browsingContext; + Assert.ok(!bc.isActive, `Tab2 iframe ${i} should be inactive`); + } + }); + + // Navigate tab 2 to a different page + BrowserTestUtils.loadURIString( + ctx.tab2Browser, + testPath + "bug343515_pg3.html" + ); + + await BrowserTestUtils.browserLoaded(ctx.tab2Browser); + + // Step 4. + + async function checkTab2Active(outerExpected) { + await SpecialPowers.spawn( + ctx.tab2Browser, + [outerExpected], + async function (expected) { + function isActive(aWindow) { + var docshell = aWindow.docShell; + info(`checking ${docshell.browsingContext.id}`); + return docshell.browsingContext.isActive; + } + + let active = expected ? "active" : "inactive"; + Assert.equal(content.frames.length, 2, "Tab 2 should have 2 iframes"); + for (var i = 0; i < content.frames.length; i++) { + info("step 4, frame " + i + " info: " + content.frames[i].location); + } + Assert.equal( + content.frames[0].frames.length, + 1, + "Tab 2 iframe 0 should have 1 iframes" + ); + Assert.equal( + isActive(content.frames[0]), + expected, + `Tab2 iframe 0 should be ${active}` + ); + Assert.equal( + isActive(content.frames[0].frames[0]), + expected, + `Tab2 iframe 0 subiframe 0 should be ${active}` + ); + Assert.equal( + isActive(content.frames[1]), + expected, + `Tab2 iframe 1 should be ${active}` + ); + } + ); + } + + is( + testPath + "bug343515_pg3.html", + ctx.tab2Browser.currentURI.spec, + "Got expected tab 2 url in step 4" + ); + + // Tab 0 should be inactive, Tab 1 should be active + ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive"); + ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active"); + + // Tab2 and all descendants should be inactive + await checkTab2Active(false); + + // Switch to Tab 2 + await BrowserTestUtils.switchTab(gBrowser, ctx.tab2); + + // Check everything + ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive"); + ok(!ctx.tab1Browser.docShellIsActive, "Tab 1 should be inactive"); + ok(ctx.tab2Browser.docShellIsActive, "Tab 2 should be active"); + + await checkTab2Active(true); + + // Go back + let backDone = BrowserTestUtils.waitForLocationChange( + gBrowser, + testPath + "bug343515_pg2.html" + ); + ctx.tab2Browser.goBack(); + await backDone; + + // Step 5. + + // Check everything + ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive"); + ok(!ctx.tab1Browser.docShellIsActive, "Tab 1 should be inactive"); + ok(ctx.tab2Browser.docShellIsActive, "Tab 2 should be active"); + is( + testPath + "bug343515_pg2.html", + ctx.tab2Browser.currentURI.spec, + "Got expected tab 2 url in step 5" + ); + + await SpecialPowers.spawn(ctx.tab2Browser, [], async function () { + for (var i = 0; i < content.frames.length; i++) { + let bc = content.frames[i].browsingContext; + Assert.ok(bc.isActive, `Tab2 iframe ${i} should be active`); + } + }); + + // Switch to tab 1 + await BrowserTestUtils.switchTab(gBrowser, ctx.tab1); + + // Navigate to page 3 + BrowserTestUtils.loadURIString( + ctx.tab1Browser, + testPath + "bug343515_pg3.html" + ); + + await BrowserTestUtils.browserLoaded(ctx.tab1Browser); + + // Step 6. + + // Check everything + ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive"); + ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active"); + is( + testPath + "bug343515_pg3.html", + ctx.tab1Browser.currentURI.spec, + "Got expected tab 1 url in step 6" + ); + + await SpecialPowers.spawn(ctx.tab1Browser, [], async function () { + function isActive(aWindow) { + var docshell = aWindow.docShell; + info(`checking ${docshell.browsingContext.id}`); + return docshell.browsingContext.isActive; + } + + Assert.ok(isActive(content.frames[0]), "Tab1 iframe 0 should be active"); + Assert.ok( + isActive(content.frames[0].frames[0]), + "Tab1 iframe 0 subiframe 0 should be active" + ); + Assert.ok(isActive(content.frames[1]), "Tab1 iframe 1 should be active"); + }); + + ok(!ctx.tab2Browser.docShellIsActive, "Tab 2 should be inactive"); + + await SpecialPowers.spawn(ctx.tab2Browser, [], async function () { + for (var i = 0; i < content.frames.length; i++) { + let bc = content.frames[i].browsingContext; + Assert.ok(!bc.isActive, `Tab2 iframe ${i} should be inactive`); + } + }); + + // Go forward on tab 2 + let forwardDone = BrowserTestUtils.waitForLocationChange( + gBrowser, + testPath + "bug343515_pg3.html" + ); + ctx.tab2Browser.goForward(); + await forwardDone; + + // Step 7. + + async function checkBrowser(browser, outerTabNum, outerActive) { + let data = { tabNum: outerTabNum, active: outerActive }; + await SpecialPowers.spawn( + browser, + [data], + async function ({ tabNum, active }) { + function isActive(aWindow) { + var docshell = aWindow.docShell; + info(`checking ${docshell.browsingContext.id}`); + return docshell.browsingContext.isActive; + } + + let activestr = active ? "active" : "inactive"; + Assert.equal( + isActive(content.frames[0]), + active, + `Tab${tabNum} iframe 0 should be ${activestr}` + ); + Assert.equal( + isActive(content.frames[0].frames[0]), + active, + `Tab${tabNum} iframe 0 subiframe 0 should be ${activestr}` + ); + Assert.equal( + isActive(content.frames[1]), + active, + `Tab${tabNum} iframe 1 should be ${activestr}` + ); + } + ); + } + + // Check everything + ok(!ctx.tab0Browser.docShellIsActive, "Tab 0 should be inactive"); + ok(ctx.tab1Browser.docShellIsActive, "Tab 1 should be active"); + is( + testPath + "bug343515_pg3.html", + ctx.tab2Browser.currentURI.spec, + "Got expected tab 2 url in step 7" + ); + + await checkBrowser(ctx.tab1Browser, 1, true); + + ok(!ctx.tab2Browser.docShellIsActive, "Tab 2 should be inactive"); + await checkBrowser(ctx.tab2Browser, 2, false); + + // Close the tabs we made + BrowserTestUtils.removeTab(ctx.tab1); + BrowserTestUtils.removeTab(ctx.tab2); +}); diff --git a/docshell/test/navigation/browser_ghistorymaxsize_is_0.js b/docshell/test/navigation/browser_ghistorymaxsize_is_0.js new file mode 100644 index 0000000000..43c36bbaf2 --- /dev/null +++ b/docshell/test/navigation/browser_ghistorymaxsize_is_0.js @@ -0,0 +1,82 @@ +add_task(async function () { + // The urls don't really matter as long as they are of the same origin + var URL = + "http://mochi.test:8888/browser/docshell/test/navigation/bug343515_pg1.html"; + var URL2 = + "http://mochi.test:8888/browser/docshell/test/navigation/bug343515_pg3_1.html"; + + // We want to test a specific code path that leads to this call + // https://searchfox.org/mozilla-central/rev/e7c61f4a68b974d5fecd216dc7407b631a24eb8f/docshell/base/nsDocShell.cpp#10795 + // when gHistoryMaxSize is 0 and mIndex and mRequestedIndex are -1 + + // 1. Navigate to URL + await BrowserTestUtils.withNewTab( + { gBrowser, url: URL }, + async function (browser) { + // At this point, we haven't set gHistoryMaxSize to 0, and it is still 50 (default value). + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + let sh = browser.browsingContext.sessionHistory; + is( + sh.count, + 1, + "We should have entry in session history because we haven't changed gHistoryMaxSize to be 0 yet" + ); + is( + sh.index, + 0, + "Shistory's current index should be 0 because we haven't purged history yet" + ); + } else { + await ContentTask.spawn(browser, null, () => { + var sh = content.window.docShell.QueryInterface(Ci.nsIWebNavigation) + .sessionHistory.legacySHistory; + is( + sh.count, + 1, + "We should have entry in session history because we haven't changed gHistoryMaxSize to be 0 yet" + ); + is( + sh.index, + 0, + "Shistory's current index should be 0 because we haven't purged history yet" + ); + }); + } + + var loadPromise = BrowserTestUtils.browserLoaded(browser, false, URL2); + // If we set the pref at the beginning of this page, then when we launch a child process + // to navigate to URL in Step 1, because of + // https://searchfox.org/mozilla-central/rev/e7c61f4a68b974d5fecd216dc7407b631a24eb8f/docshell/shistory/nsSHistory.cpp#308-312 + // this pref will be set to the default value (currently 50). Setting this pref after the child process launches + // is a robust way to make sure it stays 0 + await SpecialPowers.pushPrefEnv({ + set: [["browser.sessionhistory.max_entries", 0]], + }); + // 2. Navigate to URL2 + // We are navigating to a page with the same origin so that we will stay in the same process + BrowserTestUtils.loadURIString(browser, URL2); + await loadPromise; + + // 3. Reload the browser with specific flags so that we end up here + // https://searchfox.org/mozilla-central/rev/e7c61f4a68b974d5fecd216dc7407b631a24eb8f/docshell/base/nsDocShell.cpp#10795 + var promise = BrowserTestUtils.browserLoaded(browser); + browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); + await promise; + + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + let sh = browser.browsingContext.sessionHistory; + is(sh.count, 0, "We should not save any entries in session history"); + is(sh.index, -1); + is(sh.requestedIndex, -1); + } else { + await ContentTask.spawn(browser, null, () => { + var sh = content.window.docShell.QueryInterface(Ci.nsIWebNavigation) + .sessionHistory.legacySHistory; + is(sh.count, 0, "We should not save any entries in session history"); + is(sh.index, -1); + is(sh.requestedIndex, -1); + }); + } + } + ); +}); diff --git a/docshell/test/navigation/browser_test-content-chromeflags.js b/docshell/test/navigation/browser_test-content-chromeflags.js new file mode 100644 index 0000000000..ab1ae41c0c --- /dev/null +++ b/docshell/test/navigation/browser_test-content-chromeflags.js @@ -0,0 +1,54 @@ +const TEST_PAGE = `data:text/html,<html><body><a href="about:blank" target="_blank">Test</a></body></html>`; +const { CHROME_ALL, CHROME_REMOTE_WINDOW, CHROME_FISSION_WINDOW } = + Ci.nsIWebBrowserChrome; + +/** + * Tests that when we open new browser windows from content they + * get the full browser chrome. + */ +add_task(async function () { + // Make sure that the window.open call will open a new + // window instead of a new tab. + await new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { + set: [["browser.link.open_newwindow", 2]], + }, + resolve + ); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async function (browser) { + let openedPromise = BrowserTestUtils.waitForNewWindow(); + BrowserTestUtils.synthesizeMouse("a", 0, 0, {}, browser); + let win = await openedPromise; + + let chromeFlags = win.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags; + + let expected = CHROME_ALL; + + // In the multi-process tab case, the new window will have the + // CHROME_REMOTE_WINDOW flag set. + if (gMultiProcessBrowser) { + expected |= CHROME_REMOTE_WINDOW; + } + + // In the multi-process subframe case, the new window will have the + // CHROME_FISSION_WINDOW flag set. + if (gFissionBrowser) { + expected |= CHROME_FISSION_WINDOW; + } + + is(chromeFlags, expected, "Window should have opened with all chrome"); + + await BrowserTestUtils.closeWindow(win); + } + ); +}); diff --git a/docshell/test/navigation/browser_test_bfcache_eviction.js b/docshell/test/navigation/browser_test_bfcache_eviction.js new file mode 100644 index 0000000000..32772fe42c --- /dev/null +++ b/docshell/test/navigation/browser_test_bfcache_eviction.js @@ -0,0 +1,102 @@ +add_task(async function () { + // We don't want the number of total viewers to be calculated by the available size + // for this test case. Instead, fix the number of viewers. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.sessionhistory.max_total_viewers", 3], + ["docshell.shistory.testing.bfevict", true], + // If Fission is disabled, the pref is no-op. + ["fission.bfcacheInParent", true], + ], + }); + + // 1. Open a tab + var testPage = + "data:text/html,<html id='html1'><body id='body1'>First tab ever opened</body></html>"; + await BrowserTestUtils.withNewTab( + { gBrowser, url: testPage }, + async function (browser) { + let testDone = {}; + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + // 2. Add a promise that will be resolved when the 'content viewer evicted' event goes off + testDone.promise = SpecialPowers.spawn(browser, [], async function () { + return new Promise(resolve => { + let webNavigation = content.docShell.QueryInterface( + Ci.nsIWebNavigation + ); + let { legacySHistory } = webNavigation.sessionHistory; + // 3. Register a session history listener to listen for a 'content viewer evicted' event. + let historyListener = { + OnContentViewerEvicted() { + ok( + true, + "History listener got called after a content viewer was evicted" + ); + legacySHistory.removeSHistoryListener(historyListener); + // 6. Resolve the promise when we got our 'content viewer evicted' event + resolve(); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + legacySHistory.addSHistoryListener(historyListener); + // Keep the weak shistory listener alive + content._testListener = historyListener; + }); + }); + } else { + // 2. Add a promise that will be resolved when the 'content viewer evicted' event goes off + testDone.promise = new Promise(resolve => { + testDone.resolve = resolve; + }); + let shistory = browser.browsingContext.sessionHistory; + // 3. Register a session history listener to listen for a 'content viewer evicted' event. + let historyListener = { + OnContentViewerEvicted() { + ok( + true, + "History listener got called after a content viewer was evicted" + ); + shistory.removeSHistoryListener(historyListener); + delete window._testListener; + // 6. Resolve the promise when we got our 'content viewer evicted' event + testDone.resolve(); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", + ]), + }; + shistory.addSHistoryListener(historyListener); + // Keep the weak shistory listener alive + window._testListener = historyListener; + } + + // 4. Open a second tab + testPage = `data:text/html,<html id='html1'><body id='body1'>I am a second tab!</body></html>`; + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + testPage + ); + + // 5. Navigate the first tab to 4 different pages. + // We should get 1 content viewer evicted because it will be outside of the range. + // If we have the following pages in our session history: P1 P2 P3 P4 P5 + // and we are currently at P5, then P1 is outside of the range + // (it is more than 3 entries away from current entry) and thus will be evicted. + for (var i = 0; i < 4; i++) { + testPage = `data:text/html,<html id='html1'><body id='body1'>${i}</body></html>`; + let pagePromise = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, testPage); + await pagePromise; + } + // 7. Wait for 'content viewer evicted' event to go off + await testDone.promise; + + // 8. Close the second tab + BrowserTestUtils.removeTab(tab2); + } + ); +}); diff --git a/docshell/test/navigation/browser_test_shentry_wireframe.js b/docshell/test/navigation/browser_test_shentry_wireframe.js new file mode 100644 index 0000000000..953a2eb6e2 --- /dev/null +++ b/docshell/test/navigation/browser_test_shentry_wireframe.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BUILDER = "http://mochi.test:8888/document-builder.sjs?html="; +const PAGE_1 = BUILDER + encodeURIComponent(`<html><body>Page 1</body></html>`); +const PAGE_2 = BUILDER + encodeURIComponent(`<html><body>Page 2</body></html>`); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.history.collectWireframes", true]], + }); +}); + +/** + * Test that capturing wireframes on nsISHEntriy's in the parent process + * happens at the right times. + */ +add_task(async function () { + await BrowserTestUtils.withNewTab(PAGE_1, async browser => { + let sh = browser.browsingContext.sessionHistory; + Assert.equal( + sh.count, + 1, + "Got the right SessionHistory entry count after initial tab load." + ); + Assert.ok( + !sh.getEntryAtIndex(0).wireframe, + "No wireframe for the loaded entry after initial tab load." + ); + + let loaded = BrowserTestUtils.browserLoaded(browser, false, PAGE_2); + BrowserTestUtils.loadURIString(browser, PAGE_2); + await loaded; + + Assert.equal( + sh.count, + 2, + "Got the right SessionHistory entry count after loading page 2." + ); + Assert.ok( + sh.getEntryAtIndex(0).wireframe, + "A wireframe was captured for the first entry after loading page 2." + ); + Assert.ok( + !sh.getEntryAtIndex(1).wireframe, + "No wireframe for the loaded entry after loading page 2." + ); + + // Now go back + loaded = BrowserTestUtils.waitForContentEvent(browser, "pageshow"); + browser.goBack(); + await loaded; + Assert.ok( + sh.getEntryAtIndex(1).wireframe, + "A wireframe was captured for the second entry after going back." + ); + Assert.ok( + !sh.getEntryAtIndex(0).wireframe, + "No wireframe for the loaded entry after going back." + ); + + // Now forward again + loaded = BrowserTestUtils.waitForContentEvent(browser, "pageshow"); + browser.goForward(); + await loaded; + + Assert.equal( + sh.count, + 2, + "Got the right SessionHistory entry count after going forward again." + ); + Assert.ok( + sh.getEntryAtIndex(0).wireframe, + "A wireframe was captured for the first entry after going forward again." + ); + Assert.ok( + !sh.getEntryAtIndex(1).wireframe, + "No wireframe for the loaded entry after going forward again." + ); + + // And using pushState + await SpecialPowers.spawn(browser, [], async () => { + content.history.pushState({}, "", "nothing-1.html"); + content.history.pushState({}, "", "nothing-2.html"); + }); + + Assert.equal( + sh.count, + 4, + "Got the right SessionHistory entry count after using pushState." + ); + Assert.ok( + sh.getEntryAtIndex(0).wireframe, + "A wireframe was captured for the first entry after using pushState." + ); + Assert.ok( + sh.getEntryAtIndex(1).wireframe, + "A wireframe was captured for the second entry after using pushState." + ); + Assert.ok( + sh.getEntryAtIndex(2).wireframe, + "A wireframe was captured for the third entry after using pushState." + ); + Assert.ok( + !sh.getEntryAtIndex(3).wireframe, + "No wireframe for the loaded entry after using pushState." + ); + + // Now check that wireframes can be written to in case we're restoring + // an nsISHEntry from serialization. + let wireframe = sh.getEntryAtIndex(2).wireframe; + sh.getEntryAtIndex(2).wireframe = null; + Assert.equal( + sh.getEntryAtIndex(2).wireframe, + null, + "Successfully cleared wireframe." + ); + + sh.getEntryAtIndex(3).wireframe = wireframe; + Assert.deepEqual( + sh.getEntryAtIndex(3).wireframe, + wireframe, + "Successfully wrote a wireframe to an nsISHEntry." + ); + }); +}); diff --git a/docshell/test/navigation/browser_test_simultaneous_normal_and_history_loads.js b/docshell/test/navigation/browser_test_simultaneous_normal_and_history_loads.js new file mode 100644 index 0000000000..a400d15466 --- /dev/null +++ b/docshell/test/navigation/browser_test_simultaneous_normal_and_history_loads.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_normal_and_history_loads() { + // This test is for the case when session history lives in the parent process. + // BFCache is disabled to get more asynchronousness. + await SpecialPowers.pushPrefEnv({ + set: [["fission.bfcacheInParent", false]], + }); + + let testPage = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + ) + "blank.html"; + await BrowserTestUtils.withNewTab( + { gBrowser, url: testPage }, + async function (browser) { + for (let i = 0; i < 2; ++i) { + BrowserTestUtils.loadURIString(browser, testPage + "?" + i); + await BrowserTestUtils.browserLoaded(browser); + } + + let sh = browser.browsingContext.sessionHistory; + is(sh.count, 3, "Should have 3 entries in the session history."); + is(sh.index, 2, "index should be 2"); + is(sh.requestedIndex, -1, "requestedIndex should be -1"); + + // The following part is racy by definition. It is testing that + // eventually requestedIndex should become -1 again. + browser.browsingContext.goToIndex(1); + let historyLoad = BrowserTestUtils.browserLoaded(browser); + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + await new Promise(r => { + setTimeout(r, 10); + }); + browser.browsingContext.loadURI( + Services.io.newURI(testPage + "?newload"), + { + triggeringPrincipal: browser.nodePrincipal, + } + ); + let newLoad = BrowserTestUtils.browserLoaded(browser); + // Note, the loads are racy. + await historyLoad; + await newLoad; + is(sh.requestedIndex, -1, "requestedIndex should be -1"); + + browser.browsingContext.goBack(); + await BrowserTestUtils.browserLoaded(browser); + is(sh.requestedIndex, -1, "requestedIndex should be -1"); + } + ); +}); diff --git a/docshell/test/navigation/bug343515_pg1.html b/docshell/test/navigation/bug343515_pg1.html new file mode 100644 index 0000000000..a8337c7f70 --- /dev/null +++ b/docshell/test/navigation/bug343515_pg1.html @@ -0,0 +1,5 @@ +<html> + <head><meta charset="UTF-8"/></head> + <body>Page 1 + </body> +</html> diff --git a/docshell/test/navigation/bug343515_pg2.html b/docshell/test/navigation/bug343515_pg2.html new file mode 100644 index 0000000000..c5f5665de5 --- /dev/null +++ b/docshell/test/navigation/bug343515_pg2.html @@ -0,0 +1,7 @@ +<html> + <head><meta charset="UTF-8"/></head> + <body>Page 2 + <iframe src="data:text/html;charset=UTF8,<html><head></head><body>pg2 iframe 0</body></html>"></iframe> + <iframe src="data:text/html;charset=UTF8,<html><head></head><body>pg2 iframe 1</body></html>"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/bug343515_pg3.html b/docshell/test/navigation/bug343515_pg3.html new file mode 100644 index 0000000000..fdc79fbf7a --- /dev/null +++ b/docshell/test/navigation/bug343515_pg3.html @@ -0,0 +1,7 @@ +<html> + <head><meta charset="UTF-8"/></head> + <body>Page 3 + <iframe src="bug343515_pg3_1.html"></iframe> + <iframe src="bug343515_pg3_2.html"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/bug343515_pg3_1.html b/docshell/test/navigation/bug343515_pg3_1.html new file mode 100644 index 0000000000..254164c9f0 --- /dev/null +++ b/docshell/test/navigation/bug343515_pg3_1.html @@ -0,0 +1,6 @@ +<html> + <head><meta charset="UTF-8"/></head> + <body>pg3 - iframe 0 + <iframe src="bug343515_pg3_1_1.html"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/bug343515_pg3_1_1.html b/docshell/test/navigation/bug343515_pg3_1_1.html new file mode 100644 index 0000000000..be05b74888 --- /dev/null +++ b/docshell/test/navigation/bug343515_pg3_1_1.html @@ -0,0 +1 @@ +<html><head><meta charset="UTF-8"/></head><body>How far does the rabbit hole go?</body></html> diff --git a/docshell/test/navigation/bug343515_pg3_2.html b/docshell/test/navigation/bug343515_pg3_2.html new file mode 100644 index 0000000000..7655eb526d --- /dev/null +++ b/docshell/test/navigation/bug343515_pg3_2.html @@ -0,0 +1 @@ +<html><head><meta charset="UTF-8"/></head><body>pg3 iframe 1</body></html> diff --git a/docshell/test/navigation/cache_control_max_age_3600.sjs b/docshell/test/navigation/cache_control_max_age_3600.sjs new file mode 100644 index 0000000000..49b6439c9f --- /dev/null +++ b/docshell/test/navigation/cache_control_max_age_3600.sjs @@ -0,0 +1,20 @@ +function handleRequest(request, response) { + let query = request.queryString; + let action = + query == "initial" + ? "cache_control_max_age_3600.sjs?second" + : "goback.html"; + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "max-age=3600"); + response.write( + "<html><head><script>window.blockBFCache = new RTCPeerConnection();</script></head>" + + '<body onload=\'opener.postMessage("loaded", "*")\'>' + + "<div id='content'>" + + new Date().getTime() + + "</div>" + + "<form action='" + + action + + "' method='POST'></form>" + + "</body></html>" + ); +} diff --git a/docshell/test/navigation/file_beforeunload_and_bfcache.html b/docshell/test/navigation/file_beforeunload_and_bfcache.html new file mode 100644 index 0000000000..5c5aea2918 --- /dev/null +++ b/docshell/test/navigation/file_beforeunload_and_bfcache.html @@ -0,0 +1,31 @@ +<html> + <head> + <script> + onpageshow = function(pageShowEvent) { + var bc = new BroadcastChannel("beforeunload_and_bfcache"); + bc.onmessage = function(event) { + if (event.data == "nextpage") { + bc.close(); + location.href += "?nextpage"; + } else if (event.data == "back") { + bc.close(); + history.back(); + } else if (event.data == "forward") { + onbeforeunload = function() { + bc.postMessage("beforeunload"); + bc.close(); + } + history.forward(); + } else if (event.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + } + }; + bc.postMessage({type: pageShowEvent.type, persisted: pageShowEvent.persisted}); + }; + </script> + </head> + <body> + </body> +</html> diff --git a/docshell/test/navigation/file_blockBFCache.html b/docshell/test/navigation/file_blockBFCache.html new file mode 100644 index 0000000000..dc743cdc67 --- /dev/null +++ b/docshell/test/navigation/file_blockBFCache.html @@ -0,0 +1,33 @@ +<script> +let keepAlive; +window.onpageshow = (pageShow) => { + let bc = new BroadcastChannel("bfcache_blocking"); + + bc.onmessage = async function(m) { + switch(m.data.message) { + case "load": + bc.close(); + location.href = m.data.url; + break; + case "runScript": + let test = new Function(`return ${m.data.fun};`)(); + keepAlive = await test.call(window); + bc.postMessage({ type: "runScriptDone" }); + break; + case "back": + bc.close(); + history.back(); + break; + case "forward": + bc.close(); + history.forward(); + break; + case "close": + window.close(); + break; + } + }; + + bc.postMessage({ type: pageShow.type, persisted: pageShow.persisted }) +}; +</script> diff --git a/docshell/test/navigation/file_bug1300461.html b/docshell/test/navigation/file_bug1300461.html new file mode 100644 index 0000000000..d7abe8be90 --- /dev/null +++ b/docshell/test/navigation/file_bug1300461.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Bug 1300461</title> + </head> + <body onload="test();"> + <script> + /** + * Bug 1300461 identifies that if a history entry was not bfcached, and + * a http redirection happens when navigating to that entry, the history + * index would mess up. + * + * The test case emulates the circumstance by the following steps + * 1) Navigate to file_bug1300461_back.html which is not bf-cachable. + * 2) In file_bug1300461_back.html, replace its own history state to + * file_bug1300461_redirect.html. + * 3) Back, and then forward. Since the document is not in bfcache, it + * tries to load file_bug1300461_redirect.html directly. + * 4) file_bug1300461_redirect.html redirects UA to + * file_bug1300461_back.html through HTTP 301 header. + * + * We verify the history index, canGoBack, canGoForward, etc. keep correct + * in this process. + */ + let Ci = SpecialPowers.Ci; + let webNav = SpecialPowers.wrap(window) + .docShell + .QueryInterface(Ci.nsIWebNavigation); + let shistory = webNav.sessionHistory; + let testSteps = [ + function() { + opener.is(shistory.count, 1, "check history length"); + opener.is(shistory.index, 0, "check history index"); + opener.ok(!webNav.canGoForward, "check canGoForward"); + setTimeout(() => window.location = "file_bug1300461_back.html", 0); + }, + function() { + opener.is(shistory.count, 2, "check history length"); + opener.is(shistory.index, 0, "check history index"); + opener.ok(webNav.canGoForward, "check canGoForward"); + window.history.forward(); + }, + function() { + opener.is(shistory.count, 2, "check history length"); + opener.is(shistory.index, 0, "check history index"); + opener.ok(webNav.canGoForward, "check canGoForward"); + opener.info("file_bug1300461.html tests finished"); + opener.finishTest(); + }, + ]; + + function test() { + if (opener) { + opener.info("file_bug1300461.html test " + opener.testCount); + testSteps[opener.testCount++](); + } + } + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1300461_back.html b/docshell/test/navigation/file_bug1300461_back.html new file mode 100644 index 0000000000..3ec431c7c1 --- /dev/null +++ b/docshell/test/navigation/file_bug1300461_back.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Bug 1300461</title> + </head> + <!-- The empty unload handler is to prevent bfcache. --> + <body onload="test();" onunload=""> + <script> + let Ci = SpecialPowers.Ci; + let webNav = SpecialPowers.wrap(window) + .docShell + .QueryInterface(Ci.nsIWebNavigation); + let shistory = webNav.sessionHistory; + async function test() { + if (opener) { + opener.info("file_bug1300461_back.html"); + opener.is(shistory.count, 2, "check history length"); + opener.is(shistory.index, 1, "check history index"); + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + opener.is(shistory.legacySHistory.requestedIndex, -1, "check requestedIndex"); + } else { + let index = await opener.getSHRequestedIndex(SpecialPowers.wrap(window).browsingContext.id); + opener.is(index, -1, "check requestedIndex"); + } + + opener.ok(webNav.canGoBack, "check canGoBack"); + if (opener.testCount == 1) { + opener.info("replaceState to redirect.html"); + window.history.replaceState({}, "", "file_bug1300461_redirect.html"); + } + window.history.back(); + } + } + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1300461_redirect.html b/docshell/test/navigation/file_bug1300461_redirect.html new file mode 100644 index 0000000000..979530c5cf --- /dev/null +++ b/docshell/test/navigation/file_bug1300461_redirect.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Bug 1300461</title> + </head> + <body> + Redirect to file_bug1300461_back.html. + </body> +</html> diff --git a/docshell/test/navigation/file_bug1300461_redirect.html^headers^ b/docshell/test/navigation/file_bug1300461_redirect.html^headers^ new file mode 100644 index 0000000000..241b891826 --- /dev/null +++ b/docshell/test/navigation/file_bug1300461_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: file_bug1300461_back.html diff --git a/docshell/test/navigation/file_bug1326251.html b/docshell/test/navigation/file_bug1326251.html new file mode 100644 index 0000000000..57a81f46f1 --- /dev/null +++ b/docshell/test/navigation/file_bug1326251.html @@ -0,0 +1,212 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Bug 1326251</title> + <script> + var bc = new BroadcastChannel("file_bug1326251"); + bc.onmessage = function(event) { + if ("nextTest" in event.data) { + testSteps[event.data.nextTest](); + } + } + + function is(val1, val2, msg) { + bc.postMessage({type: "is", value1: val1, value2: val2, message: msg}); + } + + function ok(val, msg) { + bc.postMessage({type: "ok", value: val, message: msg}); + } + + const BASE_URL = "http://mochi.test:8888/tests/docshell/test/navigation/"; + let testSteps = [ + async function() { + // Test 1: Create dynamic iframe with bfcache enabled. + // Navigate static / dynamic iframes, then navigate top level window + // and navigate back. Both iframes should still exist with history + // entries preserved. + window.onunload = null; // enable bfcache + await createDynamicFrame(document); + await loadUriInFrame(document.getElementById("staticFrame"), "frame1.html"); + await loadUriInFrame(document.getElementById("dynamicFrame"), "frame1.html"); + await loadUriInFrame(document.getElementById("staticFrame"), "frame2.html"); + await loadUriInFrame(document.getElementById("dynamicFrame"), "frame2.html"); + is(history.length, 5, "history.length"); + window.location = "goback.html"; + }, + async function() { + let webNav = SpecialPowers.wrap(window) + .docShell + .QueryInterface(SpecialPowers.Ci.nsIWebNavigation); + let shistory = webNav.sessionHistory; + is(webNav.canGoForward, true, "canGoForward"); + is(shistory.index, 4, "shistory.index"); + is(history.length, 6, "history.length"); + is(document.getElementById("staticFrame").contentWindow.location.href, BASE_URL + "frame2.html", "staticFrame location"); + is(document.getElementById("dynamicFrame").contentWindow.location.href, BASE_URL + "frame2.html", "dynamicFrame location"); + + // Test 2: Load another page in dynamic iframe, canGoForward should be + // false. + await loadUriInFrame(document.getElementById("dynamicFrame"), "frame3.html"); + is(webNav.canGoForward, false, "canGoForward"); + is(shistory.index, 5, "shistory.index"); + is(history.length, 6, "history.length"); + + // Test 3: Navigate to antoher page with bfcache disabled, all dynamic + // iframe entries should be removed. + window.onunload = function() {}; // disable bfcache + window.location = "goback.html"; + }, + async function() { + let windowWrap = SpecialPowers.wrap(window); + let docShell = windowWrap.docShell; + let shistory = docShell.QueryInterface(SpecialPowers.Ci.nsIWebNavigation) + .sessionHistory; + // Now staticFrame has frame0 -> frame1 -> frame2. + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + // *EntryIndex attributes aren't meaningful when the session history + // lives in the parent process. + is(docShell.previousEntryIndex, 3, "docShell.previousEntryIndex"); + is(docShell.loadedEntryIndex, 2, "docShell.loadedEntryIndex"); + } + is(shistory.index, 2, "shistory.index"); + is(history.length, 4, "history.length"); + is(document.getElementById("staticFrame").contentWindow.location.href, BASE_URL + "frame2.html", "staticFrame location"); + ok(!document.getElementById("dynamicFrame"), "dynamicFrame should not exist"); + + // Test 4: Load a nested frame in the static frame, navigate the inner + // static frame, add a inner dynamic frame and navigate the dynamic + // frame. Then navigate the outer static frame and go back. The inner + // iframe should show the last entry of inner static frame. + let staticFrame = document.getElementById("staticFrame"); + staticFrame.width = "320px"; + staticFrame.height = "360px"; + await loadUriInFrame(staticFrame, "iframe_static.html"); + let innerStaticFrame = staticFrame.contentDocument.getElementById("staticFrame"); + await loadUriInFrame(innerStaticFrame, "frame1.html"); + let innerDynamicFrame = await createDynamicFrame(staticFrame.contentDocument, "frame2.html"); + await loadUriInFrame(innerDynamicFrame, "frame3.html"); + // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static + // innerStaticFrame: frame0 -> frame1 + // innerDynamicFrame: frame2 -> frame3 + is(shistory.index, 5, "shistory.index"); + is(history.length, 6, "history.length"); + + // Wait for 2 load events - navigation and goback. + let onloadPromise = awaitOnload(staticFrame, 2); + await loadUriInFrame(staticFrame, "goback.html"); + await onloadPromise; + // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static -> goback + // innerStaticFrame: frame0 -> frame1 + is(shistory.index, 4, "shistory.index"); + is(history.length, 6, "history.length"); + innerStaticFrame = staticFrame.contentDocument.getElementById("staticFrame"); + is(innerStaticFrame.contentDocument.location.href, BASE_URL + "frame1.html", "innerStaticFrame location"); + ok(!staticFrame.contentDocument.getElementById("dynamicFrame"), "innerDynamicFrame should not exist"); + + // Test 5: Insert and navigate inner dynamic frame again with bfcache + // enabled, and navigate top level window to a special page which will + // evict bfcache then goback. Verify that dynamic entries are correctly + // removed in this case. + window.onunload = null; // enable bfcache + staticFrame.width = "320px"; + staticFrame.height = "360px"; + innerDynamicFrame = await createDynamicFrame(staticFrame.contentDocument, "frame2.html"); + await loadUriInFrame(innerDynamicFrame, "frame3.html"); + // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static + // innerStaticFrame: frame0 -> frame1 + // innerDynamicFrame: frame2 -> frame3 + is(shistory.index, 5, "shistory.index"); + is(history.length, 6, "history.length"); + window.location = "file_bug1326251_evict_cache.html"; + }, + async function() { + let windowWrap = SpecialPowers.wrap(window); + let docShell = windowWrap.docShell; + let shistory = docShell.QueryInterface(SpecialPowers.Ci.nsIWebNavigation) + .sessionHistory; + // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static + // innerStaticFrame: frame0 -> frame1 + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + // *EntryIndex attributes aren't meaningful when the session history + // lives in the parent process. + is(docShell.previousEntryIndex, 5, "docShell.previousEntryIndex"); + is(docShell.loadedEntryIndex, 4, "docShell.loadedEntryIndex"); + } + is(shistory.index, 4, "shistory.index"); + is(history.length, 6, "history.length"); + let staticFrame = document.getElementById("staticFrame"); + let innerStaticFrame = staticFrame.contentDocument.getElementById("staticFrame"); + is(innerStaticFrame.contentDocument.location.href, BASE_URL + "frame1.html", "innerStaticFrame location"); + ok(!staticFrame.contentDocument.getElementById("dynamicFrame"), "innerDynamicFrame should not exist"); + + // Test 6: Insert and navigate inner dynamic frame and then reload outer + // frame. Verify that inner dynamic frame entries are all removed. + staticFrame.width = "320px"; + staticFrame.height = "360px"; + let innerDynamicFrame = await createDynamicFrame(staticFrame.contentDocument, "frame2.html"); + await loadUriInFrame(innerDynamicFrame, "frame3.html"); + // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static + // innerStaticFrame: frame0 -> frame1 + // innerDynamicFrame: frame2 -> frame3 + is(shistory.index, 5, "shistory.index"); + is(history.length, 6, "history.length"); + let staticFrameLoadPromise = new Promise(resolve => { + staticFrame.onload = resolve; + }); + staticFrame.contentWindow.location.reload(); + await staticFrameLoadPromise; + // staticFrame: frame0 -> frame1 -> frame2 -> iframe_static + // innerStaticFrame: frame0 -> frame1 + is(shistory.index, 4, "shistory.index"); + is(history.length, 5, "history.length"); + innerStaticFrame = staticFrame.contentDocument.getElementById("staticFrame"); + is(innerStaticFrame.contentDocument.location.href, BASE_URL + "frame1.html", "innerStaticFrame location"); + ok(!staticFrame.contentDocument.getElementById("dynamicFrame"), "innerDynamicFrame should not exist"); + bc.postMessage("finishTest"); + bc.close(); + window.close(); + }, + ]; + + function awaitOnload(frame, occurances = 1) { + return new Promise(function(resolve, reject) { + let count = 0; + frame.addEventListener("load", function listener(e) { + if (++count == occurances) { + frame.removeEventListener("load", listener); + setTimeout(resolve, 0); + } + }); + }); + } + + async function createDynamicFrame(targetDocument, frameSrc = "frame0.html") { + let dynamicFrame = targetDocument.createElement("iframe"); + let onloadPromise = awaitOnload(dynamicFrame); + dynamicFrame.id = "dynamicFrame"; + dynamicFrame.src = frameSrc; + let container = targetDocument.getElementById("frameContainer"); + container.appendChild(dynamicFrame); + await onloadPromise; + return dynamicFrame; + } + + async function loadUriInFrame(frame, uri) { + let onloadPromise = awaitOnload(frame); + frame.src = uri; + return onloadPromise; + } + + function test() { + bc.postMessage("requestNextTest"); + } + </script> + </head> + <body onpageshow="test();"> + <div id="frameContainer"> + <iframe id="staticFrame" src="frame0.html"></iframe> + </div> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1326251_evict_cache.html b/docshell/test/navigation/file_bug1326251_evict_cache.html new file mode 100644 index 0000000000..b9873947f4 --- /dev/null +++ b/docshell/test/navigation/file_bug1326251_evict_cache.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Bug 1326251</title> + <script> + // Evict bfcache and then go back. + async function evictCache() { + await SpecialPowers.evictAllContentViewers(); + + history.back(); + } + </script> + </head> + <body onload="setTimeout(evictCache, 0);"> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1364364-1.html b/docshell/test/navigation/file_bug1364364-1.html new file mode 100644 index 0000000000..d4ecc42ad4 --- /dev/null +++ b/docshell/test/navigation/file_bug1364364-1.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>title</title> + </head> + <body onload="loadFramesAndNavigate();"> + <p id="content"></p> + <div id="frameContainer"> + </div> + <script type="application/javascript"> + function waitForLoad(frame) { + return new Promise(r => frame.onload = () => setTimeout(r, 0)); + } + + async function loadFramesAndNavigate() { + let dynamicFrame = document.createElement("iframe"); + dynamicFrame.src = "data:text/html,iframe1"; + document.querySelector("#frameContainer").appendChild(dynamicFrame); + await waitForLoad(dynamicFrame); + dynamicFrame.src = "data:text/html,iframe2"; + await waitForLoad(dynamicFrame); + dynamicFrame.src = "data:text/html,iframe3"; + await waitForLoad(dynamicFrame); + dynamicFrame.src = "data:text/html,iframe4"; + await waitForLoad(dynamicFrame); + dynamicFrame.src = "data:text/html,iframe5"; + await waitForLoad(dynamicFrame); + location.href = "file_bug1364364-2.html"; + } + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1364364-2.html b/docshell/test/navigation/file_bug1364364-2.html new file mode 100644 index 0000000000..6e52ecaaa9 --- /dev/null +++ b/docshell/test/navigation/file_bug1364364-2.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>title</title> + </head> + <body onload="notifyOpener();"> + <script type="application/javascript"> + function notifyOpener() { + opener.postMessage("navigation-done", "*"); + } + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1375833-frame1.html b/docshell/test/navigation/file_bug1375833-frame1.html new file mode 100644 index 0000000000..ea38326479 --- /dev/null +++ b/docshell/test/navigation/file_bug1375833-frame1.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>iframe test page 1</title> + </head> + <body>iframe test page 1</body> +</html> diff --git a/docshell/test/navigation/file_bug1375833-frame2.html b/docshell/test/navigation/file_bug1375833-frame2.html new file mode 100644 index 0000000000..6e76ab7e47 --- /dev/null +++ b/docshell/test/navigation/file_bug1375833-frame2.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>iframe test page 2</title> + </head> + <body>iframe test page 2</body> +</html> diff --git a/docshell/test/navigation/file_bug1375833.html b/docshell/test/navigation/file_bug1375833.html new file mode 100644 index 0000000000..373a7fe08e --- /dev/null +++ b/docshell/test/navigation/file_bug1375833.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Test for bug 1375833</title> + </head> + <body onload="test();"> + <iframe id="testFrame" src="file_bug1375833-frame1.html"></iframe> + <script type="application/javascript"> + function test() { + let iframe = document.querySelector("#testFrame"); + setTimeout(function() { iframe.src = "file_bug1375833-frame1.html"; }, 0); + iframe.onload = function(e) { + setTimeout(function() { iframe.src = "file_bug1375833-frame2.html"; }, 0); + iframe.onload = function() { + opener.postMessage(iframe.contentWindow.location.href, "*"); + }; + }; + } + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1379762-1.html b/docshell/test/navigation/file_bug1379762-1.html new file mode 100644 index 0000000000..c8cd666667 --- /dev/null +++ b/docshell/test/navigation/file_bug1379762-1.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Bug 1379762</title> + </head> + <img srcset> <!-- This tries to add load blockers during bfcache restoration --> + <script> + onunload = null; // enable bfcache + var bc = new BroadcastChannel('bug1379762'); + bc.postMessage("init"); + onpageshow = function() { + bc.onmessage = (messageEvent) => { + let message = messageEvent.data; + if (message == "forward_back") { + // Navigate forward and then back. + // eslint-disable-next-line no-global-assign + setTimeout(function() { location = "goback.html"; }, 0); + } else if (message == "finish_test") { + // Do this async so our load event gets a chance to fire if it plans to + // do it. + setTimeout(function() { + bc.postMessage("finished"); + bc.close(); + window.close(); + }); + } + } + bc.postMessage("increment_testCount"); + }; + onload = function() { + bc.postMessage("increment_loadCount"); + }; + </script> +</html> diff --git a/docshell/test/navigation/file_bug1536471.html b/docshell/test/navigation/file_bug1536471.html new file mode 100644 index 0000000000..53012257ee --- /dev/null +++ b/docshell/test/navigation/file_bug1536471.html @@ -0,0 +1,8 @@ +<html> + <body onload="opener.bodyOnLoad()"> + Nested Frame + <div id="frameContainer"> + <iframe id="staticFrame" src="frame0.html"></iframe> + </div> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1583110.html b/docshell/test/navigation/file_bug1583110.html new file mode 100644 index 0000000000..5b08f54d21 --- /dev/null +++ b/docshell/test/navigation/file_bug1583110.html @@ -0,0 +1,26 @@ +<html> + <head> + <script> + var bc; + window.onpageshow = function(pageshow) { + bc = new BroadcastChannel("bug1583110"); + bc.onmessage = function(event) { + bc.close(); + if (event.data == "loadnext") { + location.search = "?nextpage"; + } else if (event.data == "back") { + history.back(); + } + } + bc.postMessage({type: "pageshow", persisted: pageshow.persisted }); + if (pageshow.persisted) { + document.body.appendChild(document.createElement("iframe")); + bc.close(); + window.close(); + } + } + </script> + </head> + <body> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1609475.html b/docshell/test/navigation/file_bug1609475.html new file mode 100644 index 0000000000..7699d46b08 --- /dev/null +++ b/docshell/test/navigation/file_bug1609475.html @@ -0,0 +1,51 @@ +<html> + <head> + <script> + + var loadCount = 0; + function loadListener(event) { + ++loadCount; + if (loadCount == 2) { + // Use a timer to ensure we don't get extra load events. + setTimeout(function() { + var doc1URI = document.getElementById("i1").contentDocument.documentURI; + opener.ok(doc1URI.includes("frame1.html"), + "Should have loaded the initial page to the first iframe. Got " + doc1URI); + var doc2URI = document.getElementById("i2").contentDocument.documentURI; + opener.ok(doc2URI.includes("frame1.html"), + "Should have loaded the initial page to the second iframe. Got " + doc2URI); + opener.finishTest(); + }, 1000); + } else if (loadCount > 2) { + opener.ok(false, "Too many load events"); + } + // if we don't get enough load events, the test will time out. + } + + function setupIframe(id) { + var ifr = document.getElementById(id); + return new Promise(function(resolve) { + ifr.onload = function() { + // Replace load listener to catch page loads from the session history. + ifr.onload = loadListener; + // Need to use setTimeout, because triggering loads inside + // load event listener has special behavior since at the moment + // the docshell keeps track of whether it is executing a load handler or not. + setTimeout(resolve); + } + ifr.contentWindow.location.href = "frame2.html"; + }); + } + + async function test() { + await setupIframe("i1"); + await setupIframe("i2"); + history.go(-2); + } + </script> + </head> + <body onload="setTimeout(test)"> + <iframe id="i1" src="frame1.html"></iframe> + <iframe id="i2" src="frame1.html"></iframe> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/file_bug1706090.html b/docshell/test/navigation/file_bug1706090.html new file mode 100644 index 0000000000..9c5bc025d3 --- /dev/null +++ b/docshell/test/navigation/file_bug1706090.html @@ -0,0 +1,40 @@ +<html> + <head> + <script> + onpageshow = function(pageShowEvent) { + if (location.hostname == "example.com" || + location.hostname == "test1.mochi.test") { + // BroadcastChannel doesn't work across domains, so just go to the + // previous page explicitly. + history.back(); + return; + } + + var bc = new BroadcastChannel("bug1706090"); + bc.onmessage = function(event) { + if (event.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + } else if (event.data == "sameOrigin") { + bc.close(); + location.href = location.href + "?foo" + } else if (event.data == "back") { + history.back(); + } else if (event.data == "crossOrigin") { + bc.close(); + location.href = "https://example.com:443" + location.pathname; + } else if (event.data == "sameSite") { + bc.close(); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + location.href = "http://test1.mochi.test:8888" + location.pathname; + } + } + + bc.postMessage({type: pageShowEvent.type, persisted: pageShowEvent.persisted}); + } + </script> + </head> + <body onunload="/* dummy listener*/"> + </body> +</html> diff --git a/docshell/test/navigation/file_bug1745638.html b/docshell/test/navigation/file_bug1745638.html new file mode 100644 index 0000000000..b98c8de91f --- /dev/null +++ b/docshell/test/navigation/file_bug1745638.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<script> + function go() { + var doc = document.getElementById("testFrame").contentWindow.document; + doc.open(); + doc.close(); + doc.body.innerText = "passed"; + } +</script> +<body onload="setTimeout(opener.pageLoaded);"> +The iframe below should not be blank after a reload.<br> +<iframe src="about:blank" id="testFrame"></iframe> +<script> + go(); +</script> diff --git a/docshell/test/navigation/file_bug1750973.html b/docshell/test/navigation/file_bug1750973.html new file mode 100644 index 0000000000..28b2f995ae --- /dev/null +++ b/docshell/test/navigation/file_bug1750973.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script> + function test() { + history.scrollRestoration = "manual"; + document.getElementById("initialfocus").focus(); + history.pushState('data', '', ''); + history.back(); + } + + window.onpopstate = function() { + window.onscroll = function() { + window.onscroll = null; + document.documentElement.style.display = "none"; + // focus() triggers recreation of the nsIFrames without a reflow. + document.getElementById("focustarget").focus(); + document.documentElement.style.display = ""; + // Flush the layout. + document.documentElement.getBoundingClientRect(); + opener.isnot(window.scrollY, 0, "The page should have been scrolled down(1)"); + requestAnimationFrame( + function() { + // Extra timeout to ensure we're called after rAF has triggered a + // reflow. + setTimeout(function() { + opener.isnot(window.scrollY, 0, "The page should have been scrolled down(2)"); + window.close(); + opener.SimpleTest.finish(); + }); + }); + } + window.scrollTo(0, 1000); + } + </script> +</head> +<body onload="setTimeout(test)"> +<div style="position: fixed;"> + <input type="button" value="" id="initialfocus"> + <input type="button" value="" id="focustarget"> +</div> +<div style="border: 1px solid black; width: 100px; height: 9000px;"></div> +</body> +</html> diff --git a/docshell/test/navigation/file_bug1758664.html b/docshell/test/navigation/file_bug1758664.html new file mode 100644 index 0000000000..07798dfddd --- /dev/null +++ b/docshell/test/navigation/file_bug1758664.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head> +<script> +var onIframeOnload = function() { + var iframe = window.document.getElementById('applicationIframe'); + opener.is(iframe.contentWindow.location.search, "?iframe", "Should have loaded the iframe"); + window.close(); + opener.SimpleTest.finish(); +} + +var onPageOnload = function() { + if (location.search == "?iframe") { + return; + } + if(!window.name) { + window.name = 'file_bug1758664.html'; + window.location.reload(); + return; + } + var iframe = window.document.getElementById('applicationIframe'); + iframe.addEventListener('load', onIframeOnload); + iframe.src = "file_bug1758664.html?iframe"; +} +window.document.addEventListener("DOMContentLoaded", onPageOnload); + +</script> +</head> +<body> + <iframe id="applicationIframe"></iframe> +</body> +</html> diff --git a/docshell/test/navigation/file_bug386782_contenteditable.html b/docshell/test/navigation/file_bug386782_contenteditable.html new file mode 100644 index 0000000000..4515d015d9 --- /dev/null +++ b/docshell/test/navigation/file_bug386782_contenteditable.html @@ -0,0 +1 @@ +<html><head><meta charset="utf-8"><script>window.addEventListener("pageshow", function(event) { window.opener.postMessage({persisted: event.persisted}, "*"); });</script></head><body contentEditable="true"><p>contentEditable</p></body></html>
\ No newline at end of file diff --git a/docshell/test/navigation/file_bug386782_designmode.html b/docshell/test/navigation/file_bug386782_designmode.html new file mode 100644 index 0000000000..faa063cbae --- /dev/null +++ b/docshell/test/navigation/file_bug386782_designmode.html @@ -0,0 +1 @@ +<html><head><meta charset="utf-8"><script>window.addEventListener("pageshow", function(event) { window.opener.postMessage({persisted: event.persisted}, "*"); });</script></head><body><p>designModeDocument</p></body></html>
\ No newline at end of file diff --git a/docshell/test/navigation/file_bug462076_1.html b/docshell/test/navigation/file_bug462076_1.html new file mode 100644 index 0000000000..5050e79fdc --- /dev/null +++ b/docshell/test/navigation/file_bug462076_1.html @@ -0,0 +1,55 @@ +<html> + <head> + <title>Bug 462076</title> + <script> + var srcs = [ "frame0.html", + "frame1.html", + "frame2.html", + "frame3.html" ]; + + var checkCount = 0; + + function makeFrame(index) { + var ifr = document.createElement("iframe"); + ifr.src = srcs[index]; + ifr.onload = checkFrame; + document.getElementById("container" + index).appendChild(ifr); + } + + function runTest() { + var randomNumber = Math.floor(Math.random() * 4); + for (var i = randomNumber; i < 4; ++i) { + makeFrame(i); + } + for (var k = 0; k < randomNumber; ++k) { + makeFrame(k); + } + } + + function checkFrame(evt) { + var ifr = evt.target; + opener.ok(String(ifr.contentWindow.location).includes(ifr.src), + "Wrong document loaded (" + ifr.src + ", " + + ifr.contentWindow.location + ")!"); + + if (++checkCount == 4) { + if (++opener.testCount == 10) { + opener.nextTest(); + window.close(); + } else { + window.location.reload(); + } + } + } + </script> + </head> + <body> + <div id="container0"></div> + <div id="container1"></div> + <div id="container2"></div> + <div id="container3"></div> + <script> + runTest(); + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_bug462076_2.html b/docshell/test/navigation/file_bug462076_2.html new file mode 100644 index 0000000000..63cf3de3f9 --- /dev/null +++ b/docshell/test/navigation/file_bug462076_2.html @@ -0,0 +1,52 @@ +<html> + <head> + <title>Bug 462076</title> + <script> + var srcs = [ "frame0.html", + "frame1.html", + "frame2.html", + "frame3.html" ]; + + var checkCount = 0; + + function makeFrame(index) { + var ifr = document.createElement("iframe"); + ifr.src = srcs[index]; + ifr.onload = checkFrame; + document.getElementById("container" + index).appendChild(ifr); + } + + function runTest() { + var randomNumber = Math.floor(Math.random() * 4); + for (var i = randomNumber; i < 4; ++i) { + makeFrame(i); + } + for (var k = 0; k < randomNumber; ++k) { + makeFrame(k); + } + } + + function checkFrame(evt) { + var ifr = evt.target; + opener.ok(String(ifr.contentWindow.location).includes(ifr.src), + "Wrong document loaded (" + ifr.src + ", " + + ifr.contentWindow.location + ")!"); + + if (++checkCount == 4) { + if (++opener.testCount == 10) { + opener.nextTest(); + window.close(); + } else { + window.location.reload(); + } + } + } + </script> + </head> + <body onload="runTest();"> + <div id="container0"></div> + <div id="container1"></div> + <div id="container2"></div> + <div id="container3"></div> + </body> +</html> diff --git a/docshell/test/navigation/file_bug462076_3.html b/docshell/test/navigation/file_bug462076_3.html new file mode 100644 index 0000000000..5c779d2f49 --- /dev/null +++ b/docshell/test/navigation/file_bug462076_3.html @@ -0,0 +1,52 @@ +<html> + <head> + <title>Bug 462076</title> + <script> + var srcs = [ "frame0.html", + "frame1.html", + "frame2.html", + "frame3.html" ]; + + var checkCount = 0; + + function makeFrame(index) { + var ifr = document.createElement("iframe"); + ifr.src = srcs[index]; + ifr.onload = checkFrame; + document.getElementById("container" + index).appendChild(ifr); + } + + function runTest() { + var randomNumber = Math.floor(Math.random() * 4); + for (var i = randomNumber; i < 4; ++i) { + makeFrame(i); + } + for (var k = 0; k < randomNumber; ++k) { + makeFrame(k); + } + } + + function checkFrame(evt) { + var ifr = evt.target; + opener.ok(String(ifr.contentWindow.location).includes(ifr.src), + "Wrong document loaded (" + ifr.src + ", " + + ifr.contentWindow.location + ")!"); + + if (++checkCount == 4) { + if (++opener.testCount == 10) { + opener.nextTest(); + window.close(); + } else { + window.location.reload(); + } + } + } + </script> + </head> + <body onload="setTimeout(runTest, 0);"> + <div id="container0"></div> + <div id="container1"></div> + <div id="container2"></div> + <div id="container3"></div> + </body> +</html> diff --git a/docshell/test/navigation/file_bug508537_1.html b/docshell/test/navigation/file_bug508537_1.html new file mode 100644 index 0000000000..182c085670 --- /dev/null +++ b/docshell/test/navigation/file_bug508537_1.html @@ -0,0 +1,33 @@ +<html> + <head> + <script> + function dynFrameLoad() { + var ifrs = document.getElementsByTagName("iframe"); + opener.ok(String(ifrs[0].contentWindow.location).includes(ifrs[0].src), + "Wrong document loaded (1)\n"); + opener.ok(String(ifrs[1].contentWindow.location).includes(ifrs[1].src), + "Wrong document loaded (2)\n"); + if (opener && ++opener.testCount == 1) { + window.location = "goback.html"; + } else { + opener.finishTest(); + } + } + + window.addEventListener("load", + function() { + var container = document.getElementById("t1"); + container.addEventListener("load", dynFrameLoad, true); + container.appendChild(container.appendChild(document.getElementById("i1"))); + }); + </script> + </head> + <body> + <h5>Container:</h5> + <div id="t1"></div> + <h5>Original frames:</h5> + <iframe id="i1" src="frame0.html"></iframe> + <iframe src="frame1.html"></iframe> + </body> +</html> + diff --git a/docshell/test/navigation/file_bug534178.html b/docshell/test/navigation/file_bug534178.html new file mode 100644 index 0000000000..4d77dd824b --- /dev/null +++ b/docshell/test/navigation/file_bug534178.html @@ -0,0 +1,30 @@ +<html> + <head> + <script> + + function testDone() { + document.body.firstChild.remove(); + var isOK = false; + try { + isOK = history.previous != location; + } catch (ex) { + // history.previous should throw if this is the first page in shistory. + isOK = true; + } + document.body.textContent = isOK ? "PASSED" : "FAILED"; + opener.ok(isOK, "Duplicate session history entries should have been removed!"); + opener.finishTest(); + } + function ifrload() { + setTimeout(testDone, 0); + } + function test() { + var ifr = document.getElementsByTagName("iframe")[0]; + ifr.onload = ifrload; + ifr.src = "data:text/html,doc2"; + } + </script> + </head> + <body onload="setTimeout(test, 0)"><iframe src="data:text/html,doc1"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/file_contentpolicy_block_window.html b/docshell/test/navigation/file_contentpolicy_block_window.html new file mode 100644 index 0000000000..c51e574e5e --- /dev/null +++ b/docshell/test/navigation/file_contentpolicy_block_window.html @@ -0,0 +1,5 @@ +<html> +<body> +This window should never be openend! +</body> +</html> diff --git a/docshell/test/navigation/file_docshell_gotoindex.html b/docshell/test/navigation/file_docshell_gotoindex.html new file mode 100644 index 0000000000..f3e8919822 --- /dev/null +++ b/docshell/test/navigation/file_docshell_gotoindex.html @@ -0,0 +1,42 @@ +<html> + <head> + <script> + function loaded() { + if (location.search == "") { + if (opener.loadedInitialPage) { + opener.ok(true, "got back to the initial page."); + opener.setTimeout("SimpleTest.finish();"); + window.close(); + return; + } + opener.loadedInitialPage = true; + opener.info("Loaded initial page."); + // Load another page (which is this same document, but different URL.) + location.href = location.href + "?anotherPage"; + } else { + opener.info("Loaded the second page."); + location.hash = "1"; + window.onhashchange = function() { + opener.info("hash: " + location.hash); + location.hash = "2"; + window.onhashchange = function() { + opener.info("hash: " + location.hash); + var docShell = SpecialPowers.wrap(window).docShell; + var webNavigation = + SpecialPowers.do_QueryInterface(docShell, "nsIWebNavigation"); + webNavigation.gotoIndex(history.length - 2); + window.onhashchange = function() { + opener.info("hash: " + location.hash); + webNavigation.gotoIndex(history.length - 4); + } + } + } + } + } + </script> + </head> + <body onpageshow="setTimeout(loaded)"> + <a href="#1" name="1">1</a> + <a href="#2" name="2">2</a> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/file_document_write_1.html b/docshell/test/navigation/file_document_write_1.html new file mode 100644 index 0000000000..be52b60231 --- /dev/null +++ b/docshell/test/navigation/file_document_write_1.html @@ -0,0 +1,18 @@ +<html> + <head> + <script> + function start() { + var length = history.length; + document.open(); + document.write("<h5 id='dynamic'>document.written content</h5>"); + document.close(); + opener.is(history.length, length, + "document.open/close should not change history"); + opener.finishTest(); + } + </script> + </head> + <body onload="start();"> + <h5>static content</h5> + </body> +</html> diff --git a/docshell/test/navigation/file_evict_from_bfcache.html b/docshell/test/navigation/file_evict_from_bfcache.html new file mode 100644 index 0000000000..9f50533543 --- /dev/null +++ b/docshell/test/navigation/file_evict_from_bfcache.html @@ -0,0 +1,29 @@ +<html> + <head> + <script> + onpageshow = function(pageShowEvent) { + var bc = new BroadcastChannel("evict_from_bfcache"); + bc.onmessage = function(event) { + if (event.data == "nextpage") { + bc.close(); + location.href += "?nextpage"; + } else if (event.data == "back") { + bc.close(); + history.back(); + } else if (event.data == "forward") { + // Note, we don't close BroadcastChannel + history.forward(); + } else if (event.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + } + } + + bc.postMessage({ type: "pageshow", persisted: pageShowEvent.persisted}); + }; + </script> + </head> + <body> + </body> +</html> diff --git a/docshell/test/navigation/file_fragment_handling_during_load.html b/docshell/test/navigation/file_fragment_handling_during_load.html new file mode 100644 index 0000000000..a7f468c32d --- /dev/null +++ b/docshell/test/navigation/file_fragment_handling_during_load.html @@ -0,0 +1,27 @@ +<html> + <head> + <script> + function checkHaveLoadedNewDoc() { + let l = document.body.firstChild.contentWindow.location.href; + if (!l.endsWith("file_fragment_handling_during_load_frame2.sjs")) { + opener.ok(true, "Fine. We will check later."); + setTimeout(checkHaveLoadedNewDoc, 500); + return; + } + opener.ok(true, "Have loaded a new document."); + opener.finishTest(); + } + function test() { + // Test that executing back() before load has started doesn't interrupt + // the load. + var ifr = document.getElementsByTagName("iframe")[0]; + ifr.onload = checkHaveLoadedNewDoc; + ifr.contentWindow.location.hash = "b"; + ifr.contentWindow.location.href = "file_fragment_handling_during_load_frame2.sjs"; + history.back(); + } + </script> + </head> + <body onload="setTimeout(test, 0)"><iframe src="file_fragment_handling_during_load_frame1.html#a"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/file_fragment_handling_during_load_frame1.html b/docshell/test/navigation/file_fragment_handling_during_load_frame1.html new file mode 100644 index 0000000000..c03ba2bda6 --- /dev/null +++ b/docshell/test/navigation/file_fragment_handling_during_load_frame1.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> +foo +</body> +</html> diff --git a/docshell/test/navigation/file_fragment_handling_during_load_frame2.sjs b/docshell/test/navigation/file_fragment_handling_during_load_frame2.sjs new file mode 100644 index 0000000000..77abe5949e --- /dev/null +++ b/docshell/test/navigation/file_fragment_handling_during_load_frame2.sjs @@ -0,0 +1,20 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + // Wait a bit. + var s = Date.now(); + // eslint-disable-next-line no-empty + while (Date.now() - s < 1000) {} + + response.write(`<!DOCTYPE HTML> + <html> + <body> + bar + </body> + </html> + `); +} diff --git a/docshell/test/navigation/file_load_history_entry_page_with_one_link.html b/docshell/test/navigation/file_load_history_entry_page_with_one_link.html new file mode 100644 index 0000000000..a4d1b62176 --- /dev/null +++ b/docshell/test/navigation/file_load_history_entry_page_with_one_link.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<body onpageshow="opener.bodyOnLoad()"> +<a id="link1" href="#1">Link 1</a> +<a name="1">link 1</a> +</body> +</html> diff --git a/docshell/test/navigation/file_load_history_entry_page_with_two_links.html b/docshell/test/navigation/file_load_history_entry_page_with_two_links.html new file mode 100644 index 0000000000..4be2ea6f4e --- /dev/null +++ b/docshell/test/navigation/file_load_history_entry_page_with_two_links.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<body onpageshow="opener.bodyOnLoad()"> +<a id="link1" href="#1">Link 1</a> +<a id="link2" href="#2">Link 2</a> +<a name="1">link 1</a> +<a name="2">link 2</a> +</body> +</html> diff --git a/docshell/test/navigation/file_meta_refresh.html b/docshell/test/navigation/file_meta_refresh.html new file mode 100644 index 0000000000..2a06cc5acf --- /dev/null +++ b/docshell/test/navigation/file_meta_refresh.html @@ -0,0 +1,40 @@ +<html> + <head> + </head> + <body> + <script> + function addMetaRefresh() { + // eslint-disable-next-line no-unsanitized/property + document.head.innerHTML = "<meta http-equiv='refresh' content='5; url=" + + location.href.replace("?initial", "?refresh") + "'>"; + } + + onpageshow = function() { + let bc = new BroadcastChannel("test_meta_refresh"); + bc.onmessage = function(event) { + if (event.data == "loadnext") { + bc.close(); + addMetaRefresh(); + location.href = location.href.replace("?initial", "?nextpage"); + } else if (event.data == "back") { + bc.close(); + history.back(); + } else if (event.data == "ensuremetarefresh") { + bc.close(); + // This test is designed to work with and without bfcache, but + // if bfcache is enabled, meta refresh should be suspended/resumed. + if (document.head.firstChild.localName != "meta") { + addMetaRefresh(); + } + } else if (event.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + } + }; + + bc.postMessage({ load: location.search.substr(1) }); + } + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_navigation_type.html b/docshell/test/navigation/file_navigation_type.html new file mode 100644 index 0000000000..bb538eefec --- /dev/null +++ b/docshell/test/navigation/file_navigation_type.html @@ -0,0 +1,25 @@ +<html> + <head> + <script> + onpageshow = function() { + let bc = new BroadcastChannel("navigation_type"); + bc.onmessage = function(event) { + if (event.data == "loadNewPage") { + bc.close(); + location.href = location.href + "?next"; + } else if (event.data == "back") { + bc.close(); + history.back(); + } else if (event.data == "close") { + window.close(); + bc.postMessage("closed"); + bc.close(); + } + } + bc.postMessage({ navigationType: performance.navigation.type }); + } + </script> + </head> + <body> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/file_nested_frames.html b/docshell/test/navigation/file_nested_frames.html new file mode 100644 index 0000000000..6ec286aa3e --- /dev/null +++ b/docshell/test/navigation/file_nested_frames.html @@ -0,0 +1,27 @@ +<html> + <head> + <script> + function nestedIframeLoaded() { + var tf = document.getElementById("testframe"); + var innerf = tf.contentDocument.getElementsByTagName("iframe")[0]; + if (!innerf.contentDocument.documentURI.includes("frame0")) { + innerf.contentWindow.location.href = "http://mochi.test:8888/tests/docshell/test/navigation/frame0.html"; + return; + } + innerf.onload = null; + innerf.src = "about:blank"; + var d = innerf.contentDocument; + d.open(); + d.write("test"); + d.close(); + opener.is(window.history.length, 1, "Unexpected history length"); + opener.finishTest(); + } + </script> + </head> + <body> + <iframe id="testframe" src="file_nested_frames_innerframe.html" onload="frameLoaded()"></iframe> + <script> + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_nested_frames_innerframe.html b/docshell/test/navigation/file_nested_frames_innerframe.html new file mode 100644 index 0000000000..e25b6a4f6a --- /dev/null +++ b/docshell/test/navigation/file_nested_frames_innerframe.html @@ -0,0 +1 @@ +<iframe onload='parent.nestedIframeLoaded();'></iframe> diff --git a/docshell/test/navigation/file_nested_srcdoc.html b/docshell/test/navigation/file_nested_srcdoc.html new file mode 100644 index 0000000000..ae6d656f27 --- /dev/null +++ b/docshell/test/navigation/file_nested_srcdoc.html @@ -0,0 +1,3 @@ + +iframe loaded inside of a srcdoc +<iframe id="static" srcdoc="Second nested srcdoc<iframe id='static' srcdoc='Third nested srcdoc'></iframe>"></iframe> diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html b/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html new file mode 100644 index 0000000000..2f9a41e1d1 --- /dev/null +++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html @@ -0,0 +1,5 @@ +<script> + onload = function() { + opener.postMessage("load"); + } +</script> diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html^headers^ b/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_1.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html b/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html new file mode 100644 index 0000000000..de456f8f1c --- /dev/null +++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html @@ -0,0 +1,10 @@ +<script> + onload = function() { + opener.postMessage("load"); + } + + onbeforeunload = function() { + opener.postMessage("beforeunload"); + history.pushState("data1", "", "?push1"); + } +</script> diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html^headers^ b/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_2.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html b/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html new file mode 100644 index 0000000000..2237e3e367 --- /dev/null +++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html @@ -0,0 +1,22 @@ +<script> + window.onpageshow = function(e) { + if (location.search == "?v2") { + onbeforeunload = function() { + history.pushState("data1", "", "?push1"); + } + } + + let bc = new BroadcastChannel("new_shentry_during_history_navigation"); + bc.onmessage = function(event) { + bc.close(); + if (event.data == "loadnext") { + location.href = "file_new_shentry_during_history_navigation_4.html"; + } else if (event.data == "back") { + history.back(); + } else if (event.data == "close") { + window.close(); + } + } + bc.postMessage({page: location.href, type: e.type, persisted: e.persisted}); + } +</script> diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html^headers^ b/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_3.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/navigation/file_new_shentry_during_history_navigation_4.html b/docshell/test/navigation/file_new_shentry_during_history_navigation_4.html new file mode 100644 index 0000000000..d5c3f61794 --- /dev/null +++ b/docshell/test/navigation/file_new_shentry_during_history_navigation_4.html @@ -0,0 +1,16 @@ +<script> + window.onpageshow = function(e) { + let bc = new BroadcastChannel("new_shentry_during_history_navigation"); + bc.onmessage = function(event) { + bc.close(); + if (event.data == "loadnext") { + location.href = "file_new_shentry_during_history_navigation_3.html?v2"; + } else if (event.data == "forward") { + history.forward(); + } else if (event.data == "close") { + window.close(); + } + } + bc.postMessage({page: location.href, type: e.type, persisted: e.persisted}); + } +</script> diff --git a/docshell/test/navigation/file_online_offline_bfcache.html b/docshell/test/navigation/file_online_offline_bfcache.html new file mode 100644 index 0000000000..7f8138e758 --- /dev/null +++ b/docshell/test/navigation/file_online_offline_bfcache.html @@ -0,0 +1,41 @@ +<html> + <head> + <script> + onpageshow = function(pageshowEvent) { + let bc = new BroadcastChannel("online_offline_bfcache"); + bc.onmessage = function(event) { + if (event.data == "nextpage") { + bc.close(); + location.href = location.href + "?nextpage"; + } else if (event.data == "back") { + bc.close(); + history.back(); + } else if (event.data == "forward") { + bc.close(); + history.forward(); + } else if (event.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + } + }; + bc.postMessage({ event: pageshowEvent.type, persisted: pageshowEvent.persisted }); + } + + onoffline = function(event) { + let bc = new BroadcastChannel("online_offline_bfcache"); + bc.postMessage(event.type); + bc.close(); + } + + ononline = function(event) { + let bc = new BroadcastChannel("online_offline_bfcache"); + bc.postMessage(event.type); + bc.close(); + } + + </script> + </head> + <body> + </body> +</html> diff --git a/docshell/test/navigation/file_reload.html b/docshell/test/navigation/file_reload.html new file mode 100644 index 0000000000..f0cb1c2d52 --- /dev/null +++ b/docshell/test/navigation/file_reload.html @@ -0,0 +1,23 @@ +<html> + <head> + <script> + window.onpageshow = function() { + let bc = new BroadcastChannel("test_reload"); + bc.onmessage = function(event) { + if (event.data == "reload") { + bc.close(); + location.reload(true); + } else if (event.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + } + } + + bc.postMessage("pageshow"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/docshell/test/navigation/file_reload_large_postdata.sjs b/docshell/test/navigation/file_reload_large_postdata.sjs new file mode 100644 index 0000000000..385d43d99f --- /dev/null +++ b/docshell/test/navigation/file_reload_large_postdata.sjs @@ -0,0 +1,46 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +Cu.importGlobalProperties(["URLSearchParams"]); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + let rv = (() => { + try { + if (request.method != "POST") { + return "ERROR: not a POST request"; + } + + let body = new URLSearchParams( + readStream(new BinaryInputStream(request.bodyInputStream)) + ); + return body.get("payload").length; + } catch (e) { + return "ERROR: Exception: " + e; + } + })(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + response.write(`<!DOCTYPE HTML> +<script> +let rv = (${JSON.stringify(rv)}); +opener.postMessage(rv, "*"); +</script> + `); +} diff --git a/docshell/test/navigation/file_reload_nonbfcached_srcdoc.sjs b/docshell/test/navigation/file_reload_nonbfcached_srcdoc.sjs new file mode 100644 index 0000000000..e1ef75efa0 --- /dev/null +++ b/docshell/test/navigation/file_reload_nonbfcached_srcdoc.sjs @@ -0,0 +1,27 @@ +const createPage = function (msg) { + return ` +<html> +<script> + onpageshow = function() { + opener.postMessage(document.body.firstChild.contentDocument.body.textContent); + } +</script> +<body><iframe srcdoc="${msg}"></iframe><body> +</html> +`; +}; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-store"); + response.setHeader("Content-Type", "text/html"); + + let currentState = getState("reload_nonbfcached_srcdoc"); + let srcdoc = "pageload:" + currentState; + if (currentState != "second") { + setState("reload_nonbfcached_srcdoc", "second"); + } else { + setState("reload_nonbfcached_srcdoc", ""); + } + + response.write(createPage(srcdoc)); +} diff --git a/docshell/test/navigation/file_same_url.html b/docshell/test/navigation/file_same_url.html new file mode 100644 index 0000000000..72a1dd2564 --- /dev/null +++ b/docshell/test/navigation/file_same_url.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<head> +<script> +let bc = new BroadcastChannel("test_same_url"); +let listener = e => { + switch (e.data) { + case "linkClick": + var link = document.getElementById("link1"); + link.click(); + break; + case "closeWin": + self.close(); + break; + } +}; +bc.addEventListener("message", listener); +</script> +</head> +<body onpageshow="bc.postMessage({bodyOnLoad: history.length})"> +<a id="link1" href="file_same_url.html">Link 1</a> +<a name="1">link 1</a> +</body> +</html> diff --git a/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache.html b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache.html new file mode 100644 index 0000000000..fec51f821d --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache.html @@ -0,0 +1,30 @@ +<html> + <head> + <script> + // Ensure layout is flushed before doing anything with scrolling. + function flushAndExecute(callback) { + window.requestAnimationFrame(function() { + setTimeout(callback); + }); + } + + var bc = new BroadcastChannel("bfcached"); + bc.onmessage = (msgEvent) => { + if (msgEvent.data == "loadNext") { + flushAndExecute(() => { + location.href = "file_scrollRestoration_bfcache_and_nobfcache_part2.html"; + }); + } else if (msgEvent.data == "forward") { + flushAndExecute(() => { + history.forward(); + }); + } + }; + window.onpageshow = (event) => { + bc.postMessage({command: "pageshow", persisted: event.persisted}); + }; + </script> + </head> + <body> + </body> +</html> diff --git a/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html new file mode 100644 index 0000000000..40e0578515 --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html @@ -0,0 +1,35 @@ +<html> + <head> + <script> + // Note, this page does not enter bfcache because of an HTTP header. + + // Ensure layout is flushed before doing anything with scrolling. + function flushAndExecute(callback) { + window.requestAnimationFrame(function() { + setTimeout(callback); + }); + } + + var bc = new BroadcastChannel("notbfcached"); + bc.onmessage = (msgEvent) => { + if (msgEvent.data == "scroll") { + flushAndExecute(() => { window.scrollTo(0, 4000); }); + } else if (msgEvent.data == "getScrollY") { + flushAndExecute(() => { bc.postMessage({ scrollY: window.scrollY}); }); + } else if (msgEvent.data == "back") { + flushAndExecute(() => { bc.close(); history.back(); }); + } else if (msgEvent.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + } + }; + window.onpageshow = (event) => { + bc.postMessage({command: "pageshow", persisted: event.persisted}); + }; + </script> + </head> + <body> + <div style="height: 5000px; border: 1px solid black;">content</div> + </body> +</html> diff --git a/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^ b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/navigation/file_scrollRestoration_navigate.html b/docshell/test/navigation/file_scrollRestoration_navigate.html new file mode 100644 index 0000000000..ac78f0abbe --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_navigate.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<script> + var bc = new BroadcastChannel("navigate"); + window.onload = () => { + bc.onmessage = (event) => { + if (event.data.command == "navigate") { + window.location = event.data.location; + bc.close(); + } + if (event.data.command == "back") { + history.back(); + bc.close(); + } + } + bc.postMessage({command: "loaded", scrollRestoration: history.scrollRestoration}); + } +</script>
\ No newline at end of file diff --git a/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html b/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html new file mode 100644 index 0000000000..1c94899ac2 --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html @@ -0,0 +1,63 @@ +<html> + <head> + <script> + var oldHistoryObject = null; + var bc = new BroadcastChannel("bug1155730_part1"); + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "test") { + var currentCase = msg.currentCase; + test(currentCase); + } + } + + function test(currentCase) { + var assertIs = []; + var assertOk = []; + var assertIsNot = []; + switch (currentCase) { + case 1: { + assertOk.push([history.scrollRestoration, "History object has scrollRestoration property."]); + assertIs.push([history.scrollRestoration, "auto", "history.scrollRestoration's default value should be 'auto'."]); + history.scrollRestoration = "foobar"; + assertIs.push([history.scrollRestoration, "auto", "Invalid enum value should not change the value of an attribute."]); + history.scrollRestoration = "manual"; + assertIs.push([history.scrollRestoration, "manual", "Valid enum value should change the value of an attribute."]); + history.scrollRestoration = "auto"; + assertIs.push([history.scrollRestoration, "auto", "Valid enum value should change the value of an attribute."]); + bc.postMessage({command: "asserts", currentCase, assertIs, assertOk}); + document.getElementById("bottom").scrollIntoView(); + window.location.reload(false); + break; + } + case 2: { + assertIsNot.push([Math.round(window.scrollY), 0, "Should have restored scrolling."]); + assertIs.push([history.scrollRestoration, "auto", "Should have the same scrollRestoration as before reload."]); + history.scrollRestoration = "manual"; + bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot}); + window.location.reload(false); + break; + } + case 3: { + assertIs.push([window.scrollY, 0, "Should not have restored scrolling."]); + assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration as before reload."]); + bc.postMessage({command: "asserts", currentCase, assertIs}); + bc.close(); + window.close(); + break; + } + } + } + window.onpageshow = (event) => { + bc.postMessage({command: "pageshow", persisted: event.persisted}); + } + </script> + </head> + <body> + <div style="border: 1px solid black; height: 5000px;"> + </div> + <div id="bottom">Hello world</div> + <a href="#hash" name="hash">hash</a> + </body> +</html> diff --git a/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html^headers^ b/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html^headers^ new file mode 100644 index 0000000000..f944e3806d --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_part1_nobfcache.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store
\ No newline at end of file diff --git a/docshell/test/navigation/file_scrollRestoration_part2_bfcache.html b/docshell/test/navigation/file_scrollRestoration_part2_bfcache.html new file mode 100644 index 0000000000..2776e42a6e --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_part2_bfcache.html @@ -0,0 +1,57 @@ +<html> + <head> + <script> + var bc = new BroadcastChannel("bug1155730_part2"); + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "test") { + var currentCase = msg.currentCase; + test(currentCase); + } + } + + function test(currentCase) { + var assertIs = []; + var assertIsNot = []; + switch (currentCase) { + case 1: { + history.scrollRestoration = "manual"; + document.getElementById("bottom").scrollIntoView(); + window.location.href = "file_scrollRestoration_navigate.html"; + break; + } + case 2: { + assertIsNot.push([Math.round(window.scrollY), 0, "Should have kept the old scroll position."]); + assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration as before reload."]); + bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot, assert2: "assert2"}); + window.scrollTo(0, 0); + window.location.hash = "hash"; + bc.postMessage({command: "nextCase"}); + requestAnimationFrame(() => { + test(currentCase + 1); + }); + break; + } + case 3: { + assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled to #hash."]); + assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation."]); + bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot}); + bc.close(); + window.close(); + break; + } + } + } + window.onpageshow = (event) => { + bc.postMessage({command: "pageshow", persisted: event.persisted}); + } + </script> + </head> + <body> + <div style="border: 1px solid black; height: 5000px;"> + </div> + <div id="bottom">Hello world</div> + <a href="#hash" name="hash">hash</a> + </body> +</html> diff --git a/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html b/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html new file mode 100644 index 0000000000..ffc68d6ccc --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html @@ -0,0 +1,157 @@ +<html> + <head> + <script> + var oldHistoryObject = null; + var currCaseForIframe = 0; + var bc = new BroadcastChannel("bug1155730_part3"); + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "test") { + var currentCase = msg.currentCase; + test(currentCase); + } + } + + // If onpopstate event takes place, check if we need to call 'test()' + var callTest = false; + var nextCase = 0; + window.onpopstate = (e) => { + if (callTest) { + callTest = false; + setTimeout(() => { + test(nextCase); + }); + } + } + + function test(currentCase) { + var assertIs = []; + var assertOk = []; + var assertIsNot = []; + switch (currentCase) { + case 1: { + history.scrollRestoration = "manual"; + window.location.hash = "hash"; + bc.postMessage({command: "nextCase"}); + requestAnimationFrame(() => { + test(currentCase + 1); + }); + break; + } + case 2: { + assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled to #hash."]); + assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation."]); + bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot}); + window.location.href = "file_scrollRestoration_navigate.html"; + break; + } + case 3: { + assertIs.push([window.scrollY, 0, "Shouldn't have kept the old scroll position."]); + assertIs.push([history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation."]); + bc.postMessage({command: "asserts", currentCase, assertIs}); + history.scrollRestoration = "auto"; + document.getElementById("bottom").scrollIntoView(); + history.pushState({ state: "state1" }, "state1"); + history.pushState({ state: "state2" }, "state2"); + window.scrollTo(0, 0); + bc.postMessage({command: "nextCase"}); + callTest = true; + nextCase = currentCase + 1; + history.back(); // go back to state 1 + break; + } + case 4: { + assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled back to the state1's position"]); + assertIs.push([history.state.state, "state1", "Unexpected state."]); + bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot}); + + history.scrollRestoration = "manual"; + document.getElementById("bottom").scrollIntoView(); + history.pushState({ state: "state3" }, "state3"); + history.pushState({ state: "state4" }, "state4"); + window.scrollTo(0, 0); + bc.postMessage({command: "nextCase"}); + callTest = true; + nextCase = currentCase + 1; + history.back(); // go back to state 3 + break; + } + case 5: { + assertIs.push([Math.round(window.scrollY), 0, "Shouldn't have scrolled back to the state3's position"]); + assertIs.push([history.state.state, "state3", "Unexpected state."]); + + history.pushState({ state: "state5" }, "state5"); + history.scrollRestoration = "auto"; + document.getElementById("bottom").scrollIntoView(); + assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled to 'bottom'."]); + bc.postMessage({command: "asserts", currentCase, assertIs, assertIsNot}); + bc.postMessage({command: "nextCase"}); + callTest = true; + nextCase = currentCase + 1; + // go back to state 3 (state 4 was removed when state 5 was pushed) + history.back(); + break; + } + case 6: { + window.scrollTo(0, 0); + bc.postMessage({command: "nextCase"}); + callTest = true; + nextCase = currentCase + 1; + history.forward(); + break; + } + case 7: { + assertIsNot.push([Math.round(window.scrollY), 0, "Should have scrolled back to the state5's position"]); + bc.postMessage({command: "asserts", currentCase, assertIsNot}); + + var ifr = document.createElement("iframe"); + ifr.src = "data:text/html,"; + document.body.appendChild(ifr); + bc.postMessage({command: "nextCase"}); + currCaseForIframe = currentCase + 1; + ifr.onload = () => { + test(currCaseForIframe); + }; + break; + } + case 8: { + oldHistoryObject = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]).contentWindow.history; + bc.postMessage({command: "nextCase"}); + currCaseForIframe++; + document.getElementsByTagName("iframe")[0].src = "about:blank"; + break; + } + case 9: { + try { + oldHistoryObject.scrollRestoration; + assertOk.push([false, "Should have thrown an exception."]); + } catch (ex) { + assertOk.push([ex != null, "Did get an exception"]); + } + try { + oldHistoryObject.scrollRestoration = "auto"; + assertOk.push([false, "Should have thrown an exception."]); + } catch (ex) { + assertOk.push([ex != null, "Did get an exception"]); + } + bc.postMessage({command: "asserts", currentCase, assertOk}); + bc.postMessage({command: "finishing"}); + bc.close(); + window.close(); + break; + } + } + } + window.onpageshow = (event) => { + bc.postMessage({command: "pageshow", persisted: event.persisted}); + } + </script> + </head> + <body> + <div style="border: 1px solid black; height: 5000px;"> + </div> + <div id="bottom">Hello world</div> + <a href="#hash" name="hash">hash</a> + </body> +</html> diff --git a/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html^headers^ b/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html^headers^ new file mode 100644 index 0000000000..f944e3806d --- /dev/null +++ b/docshell/test/navigation/file_scrollRestoration_part3_nobfcache.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store
\ No newline at end of file diff --git a/docshell/test/navigation/file_session_history_on_redirect.html b/docshell/test/navigation/file_session_history_on_redirect.html new file mode 100644 index 0000000000..df7e99a1dd --- /dev/null +++ b/docshell/test/navigation/file_session_history_on_redirect.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <script> + window.onpagehide = function(event) { + opener.is(event.persisted, false, "Should have disabled bfcache for this test."); + } + + window.onpageshow = function(event) { + opener.pageshow(); + } + </script> +</head> +<body> +</body> +</html> diff --git a/docshell/test/navigation/file_session_history_on_redirect.html^headers^ b/docshell/test/navigation/file_session_history_on_redirect.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/navigation/file_session_history_on_redirect.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/navigation/file_session_history_on_redirect_2.html b/docshell/test/navigation/file_session_history_on_redirect_2.html new file mode 100644 index 0000000000..df7e99a1dd --- /dev/null +++ b/docshell/test/navigation/file_session_history_on_redirect_2.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <script> + window.onpagehide = function(event) { + opener.is(event.persisted, false, "Should have disabled bfcache for this test."); + } + + window.onpageshow = function(event) { + opener.pageshow(); + } + </script> +</head> +<body> +</body> +</html> diff --git a/docshell/test/navigation/file_session_history_on_redirect_2.html^headers^ b/docshell/test/navigation/file_session_history_on_redirect_2.html^headers^ new file mode 100644 index 0000000000..59ba296103 --- /dev/null +++ b/docshell/test/navigation/file_session_history_on_redirect_2.html^headers^ @@ -0,0 +1 @@ +Cache-control: no-store diff --git a/docshell/test/navigation/file_sessionhistory_iframe_removal.html b/docshell/test/navigation/file_sessionhistory_iframe_removal.html new file mode 100644 index 0000000000..f18e849942 --- /dev/null +++ b/docshell/test/navigation/file_sessionhistory_iframe_removal.html @@ -0,0 +1,37 @@ +<html> + <head> + <script> + async function wait() { + return opener.SpecialPowers.spawnChrome([], function() { /*dummy */ }); + } + async function run() { + var counter = 0; + while(true) { + // Push new history entries until we hit the limit and start removing + // entries from the front. + var len = history.length; + document.getElementById("ifr1").contentWindow.history.pushState(++counter, null, null); + await wait(); + if (len == history.length) { + opener.ok(true, "Found max length " + len); + document.getElementById("ifr2").contentWindow.history.pushState(++counter, null, null); + await wait(); + document.getElementById("ifr1").contentWindow.history.pushState(++counter, null, null); + await wait(); + opener.is(len, history.length, "Adding session history entries in different iframe should behave the same way"); + // This should remove all the history entries for ifr1, leaving just + // the ones for ifr2. + document.getElementById("ifr1").remove(); + opener.is(history.length, 2, "Should have entries for the initial load and the pushState for ifr2"); + opener.finishTest(); + break; + } + } + } + </script> + </head> + <body onload="setTimeout(run)"> + <iframe src="blank.html" id="ifr1"></iframe> + <iframe src="blank.html" id="ifr2"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/file_shiftReload_and_pushState.html b/docshell/test/navigation/file_shiftReload_and_pushState.html new file mode 100644 index 0000000000..7882143c83 --- /dev/null +++ b/docshell/test/navigation/file_shiftReload_and_pushState.html @@ -0,0 +1,28 @@ +<html> + <head> + <script> + function test() { + try { + frames[0].history.pushState({}, "state", "?pushed"); + } catch (ex) { + opener.ok(false, "history.pushState shouldn't throw"); + } + + if (!opener.shiftReloadPushStateFirstRound) { + opener.shiftReloadPushStateFirstRound = true; + window.location.reload(true); + } else { + opener.ok(true, "Did run history.push"); + opener.finishTest(); + } + } + + window.addEventListener("load", function() { setTimeout(test, 0); }); + </script> + </head> + <body> + <iframe src="frame0.html"></iframe> + <script> + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_ship_beforeunload_fired.html b/docshell/test/navigation/file_ship_beforeunload_fired.html new file mode 100644 index 0000000000..a1f416f959 --- /dev/null +++ b/docshell/test/navigation/file_ship_beforeunload_fired.html @@ -0,0 +1,37 @@ +<html> + <script> + onpageshow = function(pageShowEvent) { + var bc = new BroadcastChannel("ship_beforeunload"); + bc.onmessage = function(event) { + if (event.data.action == "register_beforeunload") { + onbeforeunload = function() { + bc.postMessage("beforeunload_fired"); + bc.close(); + } + if (event.data.loadNextPageFromSessionHistory) { + history.back(); + } else { + location.href += "?differentpage"; + } + } else if (event.data.action == "navigate_to_page_b") { + bc.close(); + if (event.data.blockBFCache) { + window.blockBFCache = new RTCPeerConnection(); + } + location.href += "?pageb"; + } else if (event.data.action == "back_to_page_b") { + if (event.data.forwardNavigateToPageB) { + history.forward(); + } else { + history.back(); + } + bc.close(); + } else if (event.data.action == "close") { + bc.close(); + window.close(); + } + } + bc.postMessage({type: pageShowEvent.type, persisted: pageShowEvent.persisted}); + } + </script> +</html> diff --git a/docshell/test/navigation/file_static_and_dynamic_1.html b/docshell/test/navigation/file_static_and_dynamic_1.html new file mode 100644 index 0000000000..e66216c41e --- /dev/null +++ b/docshell/test/navigation/file_static_and_dynamic_1.html @@ -0,0 +1,31 @@ +<html> + <head> + <script> + function test() { + var ifr = document.createElement("iframe"); + ifr.src = "frame0.html"; + document.getElementById("dynamic").appendChild(ifr); + var staticFrame = document.getElementById("staticframe"); + staticFrame.onload = window.location = "goback.html"; + staticFrame.contentWindow.location = "frame1.html"; + } + + function start() { + if (++opener.testCount == 1) { + test(); + } else { + var staticFrame = document.getElementById("staticframe"); + opener.ok(String(staticFrame.contentWindow.location).includes(staticFrame.src), + "Wrong document loaded!"); + opener.finishTest(); + } + } + </script> + </head> + <body onload="setTimeout('start()', 0)"> + <h5>Dynamic</h5> + <div id="dynamic"></div> + <h5>Static</h5> + <div id="static"><iframe id="staticframe" src="frame0.html"></iframe></div> + </body> +</html> diff --git a/docshell/test/navigation/file_tell_opener.html b/docshell/test/navigation/file_tell_opener.html new file mode 100644 index 0000000000..bd45c275e6 --- /dev/null +++ b/docshell/test/navigation/file_tell_opener.html @@ -0,0 +1,8 @@ +<html> + <body onload="bodyLoaded()">Frame 1</body> + <script> + function bodyLoaded() { + opener.postMessage("body-loaded", "*"); + } + </script> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_frame_1.html b/docshell/test/navigation/file_triggeringprincipal_frame_1.html new file mode 100644 index 0000000000..528437f892 --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_frame_1.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head><meta charset="utf-8"></head> +<body> +<b>Frame 1</b><br/> +<a href="#"" id="testlink" onclick="parent.frames[1].frames[0].location='http://test2.mochi.test:8888/tests/docshell/test/navigation/file_triggeringprincipal_subframe_nav.html'">click me</a> + +<script type="application/javascript"> + // make sure to set document.domain to the same domain as the subframe + window.onload = function() { + document.domain = "mochi.test"; + }; + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + // make sure to get the right start command, otherwise + // let the parent know and fail the test + if (event.data.start !== "startTest") { + window.removeEventListener("message", receiveMessage); + window.parent.postMessage({triggeringPrincipalURI: "false"}, "*"); + } + // click the link to navigate the subframe + document.getElementById("testlink").click(); + } +</script> + +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_frame_2.html b/docshell/test/navigation/file_triggeringprincipal_frame_2.html new file mode 100644 index 0000000000..ef7cdfc178 --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_frame_2.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> +<head><meta charset="utf-8"></head> +<body> +<b>Frame 2</b><br/> +<iframe src="http://test2.mochi.test:8888/tests/docshell/test/navigation/file_triggeringprincipal_subframe.html"></iframe> +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a.html b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a.html new file mode 100644 index 0000000000..75b2933c1b --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +Frame A +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html new file mode 100644 index 0000000000..0479f5e1e5 --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +Frame A navigated by Frame B +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html new file mode 100644 index 0000000000..e5d40b267a --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<body> +Frame B navigating Frame A + +<script type="text/javascript"> + +window.open("file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html", "framea"); + +</script> + +</body> +</html> + + diff --git a/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_base.html b/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_base.html new file mode 100644 index 0000000000..caa6b275b9 --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_base.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +base test frame +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_nav.html b/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_nav.html new file mode 100644 index 0000000000..f4a4d0e631 --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_parent_iframe_window_open_nav.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +navigated by window.open() +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_subframe.html b/docshell/test/navigation/file_triggeringprincipal_subframe.html new file mode 100644 index 0000000000..ba6b6dc09a --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_subframe.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head><meta charset='utf-8'></head> +<body> +<b>Sub Frame 2</b><br/> +<script type='application/javascript'> + // make sure to set document.domain to same domain as frame 1 + window.onload = function() { + document.domain = "mochi.test"; + // let Frame 1 know that we are ready to run the test + window.parent.parent.frames[0].postMessage({start: "startTest"}, "*"); + }; +</script> +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_subframe_nav.html b/docshell/test/navigation/file_triggeringprincipal_subframe_nav.html new file mode 100644 index 0000000000..582181c00d --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_subframe_nav.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head><meta charset="utf-8"></head> +<body onload="checkResults()"> +<b>Sub Frame 2 Navigated</b><br/> + +<script type='application/javascript'> + function checkResults() { + // query the uri of the loadingPrincipal and the TriggeringPrincipal and pass + // that information on to the parent for verification. + var channel = SpecialPowers.wrap(window).docShell.currentDocumentChannel; + var triggeringPrincipalURI = channel.loadInfo.triggeringPrincipal.asciiSpec; + var loadingPrincipalURI = channel.loadInfo.loadingPrincipal.asciiSpec; + var referrerURI = document.referrer; + window.parent.parent.postMessage({triggeringPrincipalURI, + loadingPrincipalURI, + referrerURI}, "*"); + } +</script> +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html b/docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html new file mode 100644 index 0000000000..c84e216ae8 --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head><meta charset="utf-8"></head> +<body onload="checkResults()"> +<b>SubFrame Same-Origin Navigated</b><br/> + +<script type='application/javascript'> + function checkResults() { + // query the uri of the loadingPrincipal and the TriggeringPrincipal and pass + // that information on to the parent for verification. + var channel = SpecialPowers.wrap(window).docShell.currentDocumentChannel; + var triggeringPrincipalURI = channel.loadInfo.triggeringPrincipal.asciiSpec; + var loadingPrincipalURI = channel.loadInfo.loadingPrincipal.asciiSpec; + + window.parent.postMessage({triggeringPrincipalURI, + loadingPrincipalURI}, "*"); + } +</script> +</body> +</html> diff --git a/docshell/test/navigation/file_triggeringprincipal_window_open.html b/docshell/test/navigation/file_triggeringprincipal_window_open.html new file mode 100644 index 0000000000..d0644a4d5c --- /dev/null +++ b/docshell/test/navigation/file_triggeringprincipal_window_open.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +http +</body> +</html> diff --git a/docshell/test/navigation/frame0.html b/docshell/test/navigation/frame0.html new file mode 100644 index 0000000000..93d1c9c822 --- /dev/null +++ b/docshell/test/navigation/frame0.html @@ -0,0 +1,3 @@ +<html> + <body>Frame 0</body> +</html> diff --git a/docshell/test/navigation/frame1.html b/docshell/test/navigation/frame1.html new file mode 100644 index 0000000000..4d06c09d1c --- /dev/null +++ b/docshell/test/navigation/frame1.html @@ -0,0 +1,3 @@ +<html> + <body>Frame 1</body> +</html> diff --git a/docshell/test/navigation/frame2.html b/docshell/test/navigation/frame2.html new file mode 100644 index 0000000000..7a3b5e0b9b --- /dev/null +++ b/docshell/test/navigation/frame2.html @@ -0,0 +1,3 @@ +<html> + <body>Frame 2</body> +</html> diff --git a/docshell/test/navigation/frame3.html b/docshell/test/navigation/frame3.html new file mode 100644 index 0000000000..fd24293873 --- /dev/null +++ b/docshell/test/navigation/frame3.html @@ -0,0 +1,3 @@ +<html> + <body>Frame 3</body> +</html> diff --git a/docshell/test/navigation/frame_1_out_of_6.html b/docshell/test/navigation/frame_1_out_of_6.html new file mode 100644 index 0000000000..93547cd1c4 --- /dev/null +++ b/docshell/test/navigation/frame_1_out_of_6.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 1 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_2_out_of_6.html"></iframe> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/frame_2_out_of_6.html b/docshell/test/navigation/frame_2_out_of_6.html new file mode 100644 index 0000000000..02056acce8 --- /dev/null +++ b/docshell/test/navigation/frame_2_out_of_6.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 2 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_3_out_of_6.html"></iframe> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/frame_3_out_of_6.html b/docshell/test/navigation/frame_3_out_of_6.html new file mode 100644 index 0000000000..e9dc308f6e --- /dev/null +++ b/docshell/test/navigation/frame_3_out_of_6.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 3 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/frame_4_out_of_6.html"></iframe> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/frame_4_out_of_6.html b/docshell/test/navigation/frame_4_out_of_6.html new file mode 100644 index 0000000000..66a5083e4f --- /dev/null +++ b/docshell/test/navigation/frame_4_out_of_6.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 4 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://test2.mochi.test:8888/tests/docshell/test/navigation/frame_5_out_of_6.html"></iframe> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/frame_5_out_of_6.html b/docshell/test/navigation/frame_5_out_of_6.html new file mode 100644 index 0000000000..0121f0f749 --- /dev/null +++ b/docshell/test/navigation/frame_5_out_of_6.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 5 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://example.org:80/tests/docshell/test/navigation/frame_6_out_of_6.html"></iframe> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/frame_6_out_of_6.html b/docshell/test/navigation/frame_6_out_of_6.html new file mode 100644 index 0000000000..c9827ccaae --- /dev/null +++ b/docshell/test/navigation/frame_6_out_of_6.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 6 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://example.com/tests/docshell/test/navigation/frame_1_out_of_6.html"></iframe> + </body> +</html>
\ No newline at end of file diff --git a/docshell/test/navigation/frame_load_as_example_com.html b/docshell/test/navigation/frame_load_as_example_com.html new file mode 100644 index 0000000000..a1a4e7110a --- /dev/null +++ b/docshell/test/navigation/frame_load_as_example_com.html @@ -0,0 +1,6 @@ +<html> + <body> + example.com + <iframe style="height: 100vh; width:100%;" id="static" src="http://example.org/tests/docshell/test/navigation/frame_load_as_example_org.html"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/frame_load_as_example_org.html b/docshell/test/navigation/frame_load_as_example_org.html new file mode 100644 index 0000000000..2fbb8038c9 --- /dev/null +++ b/docshell/test/navigation/frame_load_as_example_org.html @@ -0,0 +1,6 @@ +<html> + <body> + example.org + <iframe style="height: 100vh; width:100%;" id="static" src="http://example.com/tests/docshell/test/navigation/frame_load_as_example_com.html"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/frame_load_as_host1.html b/docshell/test/navigation/frame_load_as_host1.html new file mode 100644 index 0000000000..eb006b21a1 --- /dev/null +++ b/docshell/test/navigation/frame_load_as_host1.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 1 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://example.com/tests/docshell/test/navigation/frame_load_as_host2.html"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/frame_load_as_host2.html b/docshell/test/navigation/frame_load_as_host2.html new file mode 100644 index 0000000000..5457c17e9b --- /dev/null +++ b/docshell/test/navigation/frame_load_as_host2.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 2 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host3.html"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/frame_load_as_host3.html b/docshell/test/navigation/frame_load_as_host3.html new file mode 100644 index 0000000000..a9064ec867 --- /dev/null +++ b/docshell/test/navigation/frame_load_as_host3.html @@ -0,0 +1,6 @@ +<html> + <body> + Page 3 + <iframe style="height: 100vh; width: 100%;" id="static" src="http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host1.html"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/frame_recursive.html b/docshell/test/navigation/frame_recursive.html new file mode 100644 index 0000000000..835d9d63a2 --- /dev/null +++ b/docshell/test/navigation/frame_recursive.html @@ -0,0 +1,6 @@ +<html> + <body> + example.com + <iframe style="height: 100vh; width:100%;" id="static" src="http://example.com/tests/docshell/test/navigation/frame_recursive.html"></iframe> + </body> +</html> diff --git a/docshell/test/navigation/goback.html b/docshell/test/navigation/goback.html new file mode 100644 index 0000000000..ce2968374e --- /dev/null +++ b/docshell/test/navigation/goback.html @@ -0,0 +1,5 @@ +<html> + <body onload="setTimeout('window.history.go(-1)', 1000);"> + window.history.go(-1); + </body> +</html> diff --git a/docshell/test/navigation/iframe.html b/docshell/test/navigation/iframe.html new file mode 100644 index 0000000000..f8fce53c55 --- /dev/null +++ b/docshell/test/navigation/iframe.html @@ -0,0 +1,9 @@ +<html> +<body> +<script> +var src = window.location.hash.substring(1); +// eslint-disable-next-line no-unsanitized/method +document.write('<iframe src="' + src + '"></iframe>'); +</script> +</body> +</html> diff --git a/docshell/test/navigation/iframe_slow_onload.html b/docshell/test/navigation/iframe_slow_onload.html new file mode 100644 index 0000000000..e8555699bb --- /dev/null +++ b/docshell/test/navigation/iframe_slow_onload.html @@ -0,0 +1,5 @@ +<html> + <body> + <iframe id="inner" src="iframe_slow_onload_inner.html"> + </body> +</html> diff --git a/docshell/test/navigation/iframe_slow_onload_inner.html b/docshell/test/navigation/iframe_slow_onload_inner.html new file mode 100644 index 0000000000..ad39eba795 --- /dev/null +++ b/docshell/test/navigation/iframe_slow_onload_inner.html @@ -0,0 +1,19 @@ +<html> + <head> + <script> + function load() { + // Let the test page know that it can try to navigate. + top.postMessage("onload", "*"); + // We're starting an infinite loop, but we need to time out after a + // while, or the loop will keep running until shutdown. + let t0 = performance.now(); + while (performance.now() - t0 < 5000) { + document.getElementById("output").innerText = Math.random(); + } + } + </script> + </head> + <body onload="load()"> + <p id="output"></p> + </body> +</html> diff --git a/docshell/test/navigation/iframe_static.html b/docshell/test/navigation/iframe_static.html new file mode 100644 index 0000000000..1bdd1437c1 --- /dev/null +++ b/docshell/test/navigation/iframe_static.html @@ -0,0 +1,8 @@ +<html> + <body> + Nested Frame + <div id="frameContainer"> + <iframe id="staticFrame" src="frame0.html"></iframe> + </div> + </body> +</html> diff --git a/docshell/test/navigation/mochitest.ini b/docshell/test/navigation/mochitest.ini new file mode 100644 index 0000000000..1c4fe28309 --- /dev/null +++ b/docshell/test/navigation/mochitest.ini @@ -0,0 +1,255 @@ +[DEFAULT] +support-files = + NavigationUtils.js + navigation_target_url.html + navigation_target_popup_url.html + blank.html + file_bug386782_contenteditable.html + file_bug386782_designmode.html + redbox_bug430723.html + bluebox_bug430723.html + file_bug462076_1.html + file_bug462076_2.html + file_bug462076_3.html + file_bug508537_1.html + file_bug534178.html + file_document_write_1.html + file_fragment_handling_during_load.html + file_fragment_handling_during_load_frame1.html + file_fragment_handling_during_load_frame2.sjs + file_nested_frames.html + file_nested_frames_innerframe.html + file_shiftReload_and_pushState.html + file_static_and_dynamic_1.html + frame0.html + frame1.html + frame2.html + frame3.html + goback.html + iframe.html + iframe_static.html + navigate.html + open.html + parent.html + file_tell_opener.html + file_triggeringprincipal_frame_1.html + file_triggeringprincipal_frame_2.html + file_triggeringprincipal_subframe.html + file_triggeringprincipal_subframe_nav.html + file_triggeringprincipal_subframe_same_origin_nav.html + file_triggeringprincipal_window_open.html + file_triggeringprincipal_parent_iframe_window_open_base.html + file_triggeringprincipal_parent_iframe_window_open_nav.html + file_triggeringprincipal_iframe_iframe_window_open_frame_a.html + file_triggeringprincipal_iframe_iframe_window_open_frame_b.html + file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html + file_load_history_entry_page_with_one_link.html + file_load_history_entry_page_with_two_links.html + file_bug1300461.html + file_bug1300461_redirect.html + file_bug1300461_redirect.html^headers^ + file_bug1300461_back.html + file_contentpolicy_block_window.html + file_bug1326251.html + file_bug1326251_evict_cache.html + file_bug1364364-1.html + file_bug1364364-2.html + file_bug1375833.html + file_bug1375833-frame1.html + file_bug1375833-frame2.html + test_bug145971.html + file_bug1609475.html + file_bug1379762-1.html + file_scrollRestoration_bfcache_and_nobfcache.html + file_scrollRestoration_bfcache_and_nobfcache_part2.html + file_scrollRestoration_bfcache_and_nobfcache_part2.html^headers^ + file_scrollRestoration_navigate.html + file_scrollRestoration_part1_nobfcache.html + file_scrollRestoration_part1_nobfcache.html^headers^ + file_scrollRestoration_part2_bfcache.html + file_scrollRestoration_part3_nobfcache.html + file_scrollRestoration_part3_nobfcache.html^headers^ + file_session_history_on_redirect.html + file_session_history_on_redirect.html^headers^ + file_session_history_on_redirect_2.html + file_session_history_on_redirect_2.html^headers^ + redirect_handlers.sjs + frame_load_as_example_com.html + frame_load_as_example_org.html + frame_load_as_host1.html + frame_load_as_host2.html + frame_load_as_host3.html + frame_1_out_of_6.html + frame_2_out_of_6.html + frame_3_out_of_6.html + frame_4_out_of_6.html + frame_5_out_of_6.html + frame_6_out_of_6.html + frame_recursive.html + object_recursive_load.html + file_nested_srcdoc.html + +[test_aboutblank_change_process.html] +[test_beforeunload_and_bfcache.html] +support-files = file_beforeunload_and_bfcache.html +[test_bug13871.html] +skip-if = + http3 +[test_bug1583110.html] +support-files = file_bug1583110.html +[test_bug1706090.html] +support-files = file_bug1706090.html +skip-if = sessionHistoryInParent # The test is currently for the old bfcache implementation +[test_bug1745638.html] +support-files = file_bug1745638.html +[test_bug1747019.html] +support-files = + goback.html + cache_control_max_age_3600.sjs +[test_bug1750973.html] +support-files = file_bug1750973.html +[test_bug1758664.html] +support-files = file_bug1758664.html +skip-if = !sessionHistoryInParent # the old implementation behaves inconsistently +[test_bug270414.html] +skip-if = + http3 +[test_bug278916.html] +[test_bug279495.html] +[test_bug344861.html] +skip-if = toolkit == "android" +[test_bug386782.html] +[test_bug430624.html] +[test_bug430723.html] +skip-if = + (!debug && (os == 'mac' || os == 'win')) # Bug 874423 + http3 +[test_bug1364364.html] +skip-if = (os == "android") # Bug 1560378 +[test_bug1375833.html] +[test_bug1536471.html] +support-files = file_bug1536471.html +[test_blockBFCache.html] +support-files = + file_blockBFCache.html + slow.sjs + iframe_slow_onload.html + iframe_slow_onload_inner.html +skip-if = + http3 +[test_child.html] +[test_docshell_gotoindex.html] +support-files = file_docshell_gotoindex.html +[test_evict_from_bfcache.html] +support-files = file_evict_from_bfcache.html +[test_grandchild.html] +skip-if = + http3 +[test_load_history_entry.html] +[test_meta_refresh.html] +support-files = file_meta_refresh.html +[test_navigation_type.html] +support-files = file_navigation_type.html +[test_new_shentry_during_history_navigation.html] +support-files = + file_new_shentry_during_history_navigation_1.html + file_new_shentry_during_history_navigation_1.html^headers^ + file_new_shentry_during_history_navigation_2.html + file_new_shentry_during_history_navigation_2.html^headers^ + file_new_shentry_during_history_navigation_3.html + file_new_shentry_during_history_navigation_3.html^headers^ + file_new_shentry_during_history_navigation_4.html +[test_not-opener.html] +skip-if = + http3 +[test_online_offline_bfcache.html] +support-files = file_online_offline_bfcache.html +[test_opener.html] +skip-if = + fission && xorigin # Bug 1716402 - New fission platform triage + os == "linux" && bits == 64 # Bug 1572299 + win10_2004 # Bug 1572299 + win11_2009 # Bug 1797751 + fission && os == "android" # Bug 1827323 +[test_popup-navigates-children.html] +skip-if = + http3 +[test_reload.html] +support-files = file_reload.html +[test_reload_nonbfcached_srcdoc.html] +support-files = file_reload_nonbfcached_srcdoc.sjs +skip-if = + http3 +[test_reserved.html] +skip-if = + debug # bug 1263213 +[test_performance_navigation.html] +[test_same_url.html] +support-files = file_same_url.html +[test_session_history_on_redirect.html] +[test_sessionhistory.html] +skip-if = verify && (os == 'mac') && debug # Hit MOZ_CRASH(Shutdown too long, probably frozen, causing a crash.) bug 1677545 +[test_dynamic_frame_forward_back.html] +[test_sessionhistory_document_write.html] +[test_sessionhistory_iframe_removal.html] +support-files = file_sessionhistory_iframe_removal.html +[test_session_history_entry_cleanup.html] +[test_fragment_handling_during_load.html] +[test_nested_frames.html] +skip-if = + http3 +[test_shiftReload_and_pushState.html] +[test_scrollRestoration.html] +[test_bug1609475.html] +[test_bug1300461.html] +[test_bug1326251.html] +skip-if = toolkit == 'android' || sessionHistoryInParent # It relies on the bfcache +[test_bug1379762.html] +[test_state_size.html] +[test_static_and_dynamic.html] +skip-if = true # This was disabled for a few years now anyway, bug 1677544 +[test_sibling-matching-parent.html] +[test_sibling-off-domain.html] +skip-if = + http3 +[test_triggeringprincipal_frame_nav.html] +skip-if = + http3 +[test_triggeringprincipal_frame_same_origin_nav.html] +skip-if = + http3 +[test_triggeringprincipal_window_open.html] +skip-if = + http3 +[test_triggeringprincipal_parent_iframe_window_open.html] +skip-if = + http3 +[test_triggeringprincipal_iframe_iframe_window_open.html] +skip-if = + http3 +[test_contentpolicy_block_window.html] +[test_rate_limit_location_change.html] +[test_reload_large_postdata.html] +support-files = + file_reload_large_postdata.sjs +[test_recursive_frames.html] +skip-if = + http3 + fission && os == "android" # Bug 1714698 +[test_bug1699721.html] +skip-if = !fission # tests fission-only process switching behaviour +[test_ship_beforeunload_fired.html] +support-files = + file_ship_beforeunload_fired.html +skip-if = + !sessionHistoryInParent + http3 +[test_ship_beforeunload_fired_2.html] +support-files = + file_ship_beforeunload_fired.html +skip-if = !sessionHistoryInParent +[test_ship_beforeunload_fired_3.html] +support-files = + file_ship_beforeunload_fired.html +skip-if = !sessionHistoryInParent +[test_open_javascript_noopener.html] diff --git a/docshell/test/navigation/navigate.html b/docshell/test/navigation/navigate.html new file mode 100644 index 0000000000..f68123188e --- /dev/null +++ b/docshell/test/navigation/navigate.html @@ -0,0 +1,38 @@ +<html> +<head> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="NavigationUtils.js"></script> + <script> + function navigate() { + var args = window.location.hash.substring(1).split(","); + var target = args[0]; + var mechanism = args[1]; + + switch (mechanism) { + case "location": + // eslint-disable-next-line no-eval + navigateByLocation(eval(target)); + break; + case "open": + navigateByOpen(target); + break; + case "form": + navigateByForm(target); + break; + case "hyperlink": + navigateByHyperlink(target); + break; + } + } + </script> +</head> +<body onload="navigate();"> +<script> +var args = window.location.hash.substring(1).split(","); +var target = args[0]; +var mechanism = args[1]; +// eslint-disable-next-line no-unsanitized/method +document.write("target=" + target + " mechanism=" + mechanism); +</script> +</body> +</html> diff --git a/docshell/test/navigation/navigation_target_popup_url.html b/docshell/test/navigation/navigation_target_popup_url.html new file mode 100644 index 0000000000..cfe6de009d --- /dev/null +++ b/docshell/test/navigation/navigation_target_popup_url.html @@ -0,0 +1 @@ +<html><body>This is a popup</body></html> diff --git a/docshell/test/navigation/navigation_target_url.html b/docshell/test/navigation/navigation_target_url.html new file mode 100644 index 0000000000..a485e8133f --- /dev/null +++ b/docshell/test/navigation/navigation_target_url.html @@ -0,0 +1 @@ +<html><body>This frame was navigated.</body></html> diff --git a/docshell/test/navigation/object_recursive_load.html b/docshell/test/navigation/object_recursive_load.html new file mode 100644 index 0000000000..3ae9521e63 --- /dev/null +++ b/docshell/test/navigation/object_recursive_load.html @@ -0,0 +1,6 @@ +<html> + <body width="400" height="300"> + Frame 0 + <object id="static" width="400" height="300" data="http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/object_recursive_load.html"></object> + </body> +</html> diff --git a/docshell/test/navigation/open.html b/docshell/test/navigation/open.html new file mode 100644 index 0000000000..9a96a8dda7 --- /dev/null +++ b/docshell/test/navigation/open.html @@ -0,0 +1,10 @@ +<html> +<body> +<script> +var target = window.location.hash.substring(1); +// eslint-disable-next-line no-unsanitized/method +document.write("target=" + target); +window.open("navigation_target_popup_url.html", target, "width=10,height=10"); +</script> +</body> +</html> diff --git a/docshell/test/navigation/parent.html b/docshell/test/navigation/parent.html new file mode 100644 index 0000000000..74722b8bdf --- /dev/null +++ b/docshell/test/navigation/parent.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<body> +This document contains a frame. +<div><iframe src="blank.html"></iframe></div> +<script> +frames[0].name = window.name + "_child0"; +window.onload = function() { + opener.postMessage("ready", "*"); +}; +</script> +</body> +</html> + diff --git a/docshell/test/navigation/redbox_bug430723.html b/docshell/test/navigation/redbox_bug430723.html new file mode 100644 index 0000000000..c2d1f98092 --- /dev/null +++ b/docshell/test/navigation/redbox_bug430723.html @@ -0,0 +1,6 @@ +<html><head> +<script> window.addEventListener("pageshow", function() { opener.nextTest(); }); </script> +</head><body> +<div style="position:absolute; left:0px; top:0px; width:50%; height:150%; background-color:red"> +<p>This is a very tall red box.</p> +</div></body></html> diff --git a/docshell/test/navigation/redirect_handlers.sjs b/docshell/test/navigation/redirect_handlers.sjs new file mode 100644 index 0000000000..c2b39e61c9 --- /dev/null +++ b/docshell/test/navigation/redirect_handlers.sjs @@ -0,0 +1,29 @@ +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Cache-Control", "no-store", false); + + let state = getState("sessionhistory_do_redirect"); + if (state != "doredirect") { + response.setHeader("Content-Type", "text/html"); + const contents = ` + <script> + window.onpageshow = function(event) { + opener.pageshow(); + } + </script> + `; + response.write(contents); + + // The next load should do a redirect. + setState("sessionhistory_do_redirect", "doredirect"); + } else { + setState("sessionhistory_do_redirect", ""); + + response.setStatusLine("1.1", 302, "Found"); + response.setHeader( + "Location", + "file_session_history_on_redirect_2.html", + false + ); + } +} diff --git a/docshell/test/navigation/redirect_to_blank.sjs b/docshell/test/navigation/redirect_to_blank.sjs new file mode 100644 index 0000000000..b1668401ea --- /dev/null +++ b/docshell/test/navigation/redirect_to_blank.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Cache-Control", "no-store", false); + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "blank.html", false); +} diff --git a/docshell/test/navigation/slow.sjs b/docshell/test/navigation/slow.sjs new file mode 100644 index 0000000000..9720f807f6 --- /dev/null +++ b/docshell/test/navigation/slow.sjs @@ -0,0 +1,16 @@ +function handleRequest(request, response) { + response.processAsync(); + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + function () { + response.finish(); + }, + 5000, + Ci.nsITimer.TYPE_ONE_SHOT + ); + + response.setStatusLine(null, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.write("Start of the content."); +} diff --git a/docshell/test/navigation/test_aboutblank_change_process.html b/docshell/test/navigation/test_aboutblank_change_process.html new file mode 100644 index 0000000000..61325570f3 --- /dev/null +++ b/docshell/test/navigation/test_aboutblank_change_process.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<head> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<script> +// Open a window and navigate it from https://example.com to about:blank +// With fission, we should switch processes and about:blank should load in +// the same process as this test page. +// This is a crash test. +add_task(async function test_aboutblank_change_process() { + let exampleLoaded = new Promise(resolve => { + function onMessage(event) { + if (event.data == "body-loaded") { + window.removeEventListener("message", onMessage); + resolve(); + } + } + window.addEventListener("message", onMessage); + }); + let win = window.open(); + win.location = "https://example.com/tests/docshell/test/navigation/file_tell_opener.html"; + await exampleLoaded; + + win.location = "about:blank"; + + // A crash happens somewhere here when about:blank does not go via + // DocumentChannel with fission enabled + + // Wait for about:blank to load in this process + await SimpleTest.promiseWaitForCondition(() => { + try { + return win.location.href == "about:blank"; + } catch (e) { + // While the `win` still has example.com page loaded, `win.location` will + // be a cross origin object and querying win.location.href will throw a + // SecurityError. Return false as long as this is the case. + return false; + } + }) + + ok(true, "We did not crash"); + win.close(); +}); +</script> diff --git a/docshell/test/navigation/test_beforeunload_and_bfcache.html b/docshell/test/navigation/test_beforeunload_and_bfcache.html new file mode 100644 index 0000000000..6bb958c6c6 --- /dev/null +++ b/docshell/test/navigation/test_beforeunload_and_bfcache.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Loading a page from BFCache and firing beforeunload on the current page</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + /* + * This is a simple test to ensure beforeunload is fired on the current page + * when restoring a page from bfcache. + * (1) The controller page opens a new window. Another page is loaded there + * and session history is navigated back to check whether bfcache is + * enabled. If not, close message is sent and the opened window closes + * and the test ends. + * (2) beforeunload event listener is added to the page and history.forward() + * is called. The event listener should send a message to the controller + * page. + * (3) Close message is sent to close the opened window and the test finishes. + */ + + var pageshowCount = 0; + var gotBeforeUnload = false; + var bfcacheDisabled = false; + + + function executeTest() { + var bc = new BroadcastChannel("beforeunload_and_bfcache"); + bc.onmessage = function(event) { + if (event.data.type == "pageshow") { + ++pageshowCount; + if (pageshowCount == 1) { + bc.postMessage("nextpage"); + } else if (pageshowCount == 2) { + bc.postMessage("back"); + } else if (pageshowCount == 3) { + if (!event.data.persisted) { + ok(true, "BFCache not enabled"); + bfcacheDisabled = true; + bc.postMessage("close"); + return; + } + bc.postMessage("forward"); + } else if (pageshowCount == 4) { + ok(event.data.persisted, "Should have loaded a page from bfcache."); + bc.postMessage("close"); + } + } else if (event.data == "beforeunload") { + gotBeforeUnload = true; + } else if (event.data == "closed") { + isnot(bfcacheDisabled, gotBeforeUnload, + "Either BFCache shouldn't be enabled or a beforeunload event should have been fired."); + bc.close(); + SimpleTest.finish(); + } + }; + + function runTest() { + SpecialPowers.pushPrefEnv({"set": [["docshell.shistory.bfcache.allow_unload_listeners", false]]}, + function() { + window.open("file_beforeunload_and_bfcache.html", "", "noopener"); + } + ); + } + runTest(); + } + + if (isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + SpecialPowers.addPermission("storageAccessAPI", true, window.location.href).then(() => { + SpecialPowers.wrap(document).requestStorageAccess().then(() => { + SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]] + }).then(() => { + executeTest(); + }); + }); + }); + } else { + executeTest(); + } + + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_blockBFCache.html b/docshell/test/navigation/test_blockBFCache.html new file mode 100644 index 0000000000..3d4a418369 --- /dev/null +++ b/docshell/test/navigation/test_blockBFCache.html @@ -0,0 +1,294 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Blocking pages from entering BFCache</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body onload=""> +<script> + +const getUserMediaPrefs = { + set: [ + ["media.devices.insecure.enabled", true], + ["media.getusermedia.insecure.enabled", true], + ["media.navigator.permission.disabled", true], + ], +}; +const msePrefs = { + set: [ + ["media.mediasource.enabled", true], + ["media.audio-max-decode-error", 0], + ["media.video-max-decode-error", 0], + ] +}; + +const blockBFCacheTests = [ + { + name: "Request", + test: () => { + return new Promise((resolve) => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "slow.sjs"); + xhr.addEventListener("progress", () => { resolve(xhr); }, { once: true }); + xhr.send(); + }); + }, + }, + { + name: "Background request", + test: () => { + return new Promise((resolve) => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "slow.sjs"); + xhr.addEventListener("readystatechange", () => { if (xhr.readyState == xhr.HEADERS_RECEIVED) resolve(xhr); }); + xhr.send(); + }); + }, + }, + { + name: "getUserMedia", + prefs: getUserMediaPrefs, + test: () => { + return navigator.mediaDevices.getUserMedia({ audio: true, fake: true }); + }, + }, + { + name: "RTCPeerConnection", + test: () => { + let pc = new RTCPeerConnection(); + return pc.createOffer(); + }, + }, + { + name: "MSE", + prefs: msePrefs, + test: () => { + const ms = new MediaSource(); + const el = document.createElement("video"); + el.src = URL.createObjectURL(ms); + el.preload = "auto"; + return el; + }, + }, + { + name: "WebSpeech", + test: () => { + return new Promise((resolve) => { + const utterance = new SpeechSynthesisUtterance('bfcache'); + utterance.lang = 'it-IT-noend'; + utterance.addEventListener('start', () => { resolve(utterance); }) + speechSynthesis.speak(utterance); + }); + }, + }, + { + name: "WebVR", + prefs: { + set: [ + ["dom.vr.test.enabled", true], + ["dom.vr.puppet.enabled", true], + ["dom.vr.require-gesture", false], + ], + }, + test: () => { + return navigator.requestVRServiceTest(); + } + }, +]; + +if (SpecialPowers.Services.appinfo.fissionAutostart) { + blockBFCacheTests.push({ + name: "Loading OOP iframe", + test: () => { + return new Promise((resolve) => { + const el = document.body.appendChild(document.createElement("iframe")); + el.id = "frame"; + addEventListener("message", ({ data }) => { + if (data == "onload") { + resolve(); + } + }); + el.src = "https://example.com/tests/docshell/test/navigation/iframe_slow_onload.html"; + }); + }, + waitForDone: () => { + SimpleTest.requestFlakyTimeout("Test has a loop in an onload handler that runs for 5000ms, we need to make sure the loop is done before moving to the next test."); + return new Promise(resolve => { + setTimeout(resolve, 5000); + }); + }, + }); +} + +const dontBlockBFCacheTests = [ + { + name: "getUserMedia", + prefs: getUserMediaPrefs, + test: () => { + return navigator.mediaDevices.getUserMedia({ video: true, fake: true }).then(stream => { + stream.getTracks().forEach(track => track.stop()); + return stream; + }); + }, + }, +/* + Disabled because MediaKeys rely on being destroyed by the CC before they + notify their window, so the test would intermittently fail depending on + when the CC runs. + + { + name: "MSE", + prefs: msePrefs, + test: () => { + return new Promise((resolve) => { + const ms = new MediaSource(); + const el = document.createElement("video"); + ms.addEventListener("sourceopen", () => { resolve(el) }, { once: true }); + el.src = URL.createObjectURL(ms); + el.preload = "auto"; + }).then(el => { + el.src = ""; + return el; + }); + }, + }, +*/ +]; + + + +function executeTest() { + + let bc = new BroadcastChannel("bfcache_blocking"); + + function promiseMessage(type) { + return new Promise((resolve, reject) => { + bc.addEventListener("message", (e) => { + if (e.data.type == type) { + resolve(e.data); + } + }, { once: true }); + }); + } + + function promisePageShow(shouldBePersisted) { + return promiseMessage("pageshow").then(data => data.persisted == shouldBePersisted); + } + + function promisePageShowFromBFCache(e) { + return promisePageShow(true); + } + + function promisePageShowNotFromBFCache(e) { + return promisePageShow(false); + } + + function runTests(testArray, shouldBlockBFCache) { + for (const { name, prefs = {}, test, waitForDone } of testArray) { + add_task(async function() { + await SpecialPowers.pushPrefEnv(prefs, async function() { + // Load a mostly blank page that we can communicate with over + // BroadcastChannel (though it will close the BroadcastChannel after + // receiving the next "load" message, to avoid blocking BFCache). + let loaded = promisePageShowNotFromBFCache(); + window.open("file_blockBFCache.html", "", "noopener"); + await loaded; + + // Load the same page with a different URL. + loaded = promisePageShowNotFromBFCache(); + bc.postMessage({ message: "load", url: `file_blockBFCache.html?${name}_${shouldBlockBFCache}` }); + await loaded; + + // Run test script in the second page. + bc.postMessage({ message: "runScript", fun: test.toString() }); + await promiseMessage("runScriptDone"); + + // Go back to the first page (this should just come from the BFCache). + let goneBack = promisePageShowFromBFCache(); + bc.postMessage({ message: "back" }); + await goneBack; + + // Go forward again to the second page and check that it does/doesn't come + // from the BFCache. + let goneForward = promisePageShow(!shouldBlockBFCache); + bc.postMessage({ message: "forward" }); + let result = await goneForward; + ok(result, `Page ${shouldBlockBFCache ? "should" : "should not"} have been blocked from going into the BFCache (${name})`); + + // If the test will keep running after navigation, then we need to make + // sure it's completely done before moving to the next test, to avoid + // interfering with any following tests. If waitForDone is defined then + // it'll return a Promise that we can use to wait for the end of the + // test. + if (waitForDone) { + await waitForDone(); + } + + // Do a similar test, but replace the bfcache test page with a new page, + // not a page coming from the session history. + + // Load the same page with a different URL. + loaded = promisePageShowNotFromBFCache(); + bc.postMessage({ message: "load", url: `file_blockBFCache.html?p2_${name}_${shouldBlockBFCache}` }); + await loaded; + + // Run the test script. + bc.postMessage({ message: "runScript", fun: test.toString() }); + await promiseMessage("runScriptDone"); + + // Load a new page. + loaded = promisePageShowNotFromBFCache(); + bc.postMessage({ message: "load", url: "file_blockBFCache.html" }); + await loaded; + + // Go back to the previous page and check that it does/doesn't come + // from the BFCache. + goneBack = promisePageShow(!shouldBlockBFCache); + bc.postMessage({ message: "back" }); + result = await goneBack; + ok(result, `Page ${shouldBlockBFCache ? "should" : "should not"} have been blocked from going into the BFCache (${name})`); + + if (waitForDone) { + await waitForDone(); + } + + bc.postMessage({ message: "close" }); + + SpecialPowers.popPrefEnv(); + }); + }); + } + } + + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + runTests(blockBFCacheTests, true); + runTests(dontBlockBFCacheTests, false); + }); +} + +if (isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + SpecialPowers.addPermission("storageAccessAPI", true, window.location.href).then(() => { + SpecialPowers.wrap(document).requestStorageAccess().then(() => { + SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]] + }).then(() => { + executeTest(); + }); + }); + }); +} else { + executeTest(); +} + +</script> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1300461.html b/docshell/test/navigation/test_bug1300461.html new file mode 100644 index 0000000000..22783c07c2 --- /dev/null +++ b/docshell/test/navigation/test_bug1300461.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 1300461</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1300461">Mozilla Bug 1300461</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + + let chromeScript = null; + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + function doSend(message, fn) { + try { + sendAsyncMessage(message, {success: true, value: fn()}); + } catch(_) { + sendAsyncMessage(message, {success: false}); + } + } + + addMessageListener("requestedIndex", (id) => { + doSend("requestedIndex", () => { + let shistory = BrowsingContext.get(id).top.sessionHistory; + return shistory.requestedIndex; + }) + }); + }); + } + + async function getSHRequestedIndex(browsingContextId) { + let p = chromeScript.promiseOneMessage("requestedIndex"); + chromeScript.sendAsyncMessage("requestedIndex", browsingContextId); + let result = await p; + ok(result.success, "Got requested index from parent"); + return result.value; + } + + var testCount = 0; + + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_bug1300461.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + if (chromeScript) { + chromeScript.destroy(); + } + testWindow.close(); + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1326251.html b/docshell/test/navigation/test_bug1326251.html new file mode 100644 index 0000000000..3c951729e6 --- /dev/null +++ b/docshell/test/navigation/test_bug1326251.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 1326251</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1326251">Mozilla Bug 1326251</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + + var testCount = 0; + + var bc = new BroadcastChannel("file_bug1326251"); + bc.onmessage = function(event) { + if (event.data == "requestNextTest") { + bc.postMessage({ nextTest: testCount++ }); + } else if (event.data.type == "is") { + is(event.data.value1, event.data.value2, event.data.message); + } else if (event.data.type == "ok") { + ok(event.data.value, event.data.message); + } else if (event.data == "finishTest") { + SimpleTest.finish(); + } + } + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + window.open("file_bug1326251.html", "", "width=360,height=480,noopener"); + }); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1364364.html b/docshell/test/navigation/test_bug1364364.html new file mode 100644 index 0000000000..90d2009df4 --- /dev/null +++ b/docshell/test/navigation/test_bug1364364.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1364364 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1364364</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1364364 */ + let testWin, testDoc; + async function test() { + SimpleTest.waitForExplicitFinish(); + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + // This test relies on the possibility to modify the bfcached document. + // The new implementation is more restricted and thus works only + // when the bfcached browsing context is the only top level one + // in the browsing context group. + ok(true, "This test is for the old bfcache implementation only."); + SimpleTest.finish(); + return; + } + testWin = window.open("file_bug1364364-1.html"); + await waitForLoad(testWin); + testDoc = testWin.document; + + // file_bug1364364-1.html will load a few dynamic iframes and then navigate + // top browsing context to file_bug1364364-2.html, which will postMessage + // back. + } + + function waitForLoad(win) { + return new Promise(r => win.addEventListener("load", r, { once: true})); + } + + window.addEventListener("message", async function(msg) { + if (msg.data == "navigation-done") { + is(testWin.history.length, 6, "check history.length"); + + // Modify a document in bfcache should cause the cache being dropped tho + // RemoveFromBFCacheAsync. + testDoc.querySelector("#content").textContent = "modified"; + await new Promise(r => setTimeout(r, 0)); + + is(testWin.history.length, 2, "check history.length after bfcache dropped"); + testWin.close(); + SimpleTest.finish(); + } + }); + + </script> +</head> +<body onload="test();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1364364">Mozilla Bug 1364364</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1375833.html b/docshell/test/navigation/test_bug1375833.html new file mode 100644 index 0000000000..c2a7750a4e --- /dev/null +++ b/docshell/test/navigation/test_bug1375833.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1375833 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1375833</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + /** + * Test for Bug 1375833. It tests for 2 things in a normal reload - + * 1. Static frame history should not be dropped. + * 2. In a reload, docshell would parse the reloaded root document and + * genearate new child docshells, and then use the child offset + */ + + let testWin = window.open("file_bug1375833.html"); + let count = 0; + let webNav, shistory; + let frameDocShellId; + let chromeScript = null; + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + function doSend(message, fn) { + try { + sendAsyncMessage(message, {success: true, value: fn()}); + } catch(_) { + sendAsyncMessage(message, {success: false}); + } + } + + addMessageListener("test1", id => { + doSend("test1", () => { + let sessionHistory = BrowsingContext.get(id).top.sessionHistory; + let entry = sessionHistory.getEntryAtIndex(sessionHistory.index); + let frameEntry = entry.GetChildAt(0); + return String(frameEntry.docshellID); + }) + }); + }); + } + + window.addEventListener("message", async e => { + switch (count++) { + case 0: + ok(e.data.endsWith("file_bug1375833-frame2.html"), "check location"); + + webNav = SpecialPowers.wrap(testWin) + .docShell + .QueryInterface(SpecialPowers.Ci.nsIWebNavigation); + shistory = webNav.sessionHistory; + is(shistory.count, 2, "check history length"); + is(shistory.index, 1, "check history index"); + + frameDocShellId = String(getFrameDocShell().historyID); + ok(frameDocShellId, "sanity check for docshell ID"); + + testWin.location.reload(); + break; + case 1: + ok(e.data.endsWith("file_bug1375833-frame2.html"), "check location"); + is(shistory.count, 4, "check history length"); + is(shistory.index, 3, "check history index"); + + let newFrameDocShellId = String(getFrameDocShell().historyID); + ok(newFrameDocShellId, "sanity check for docshell ID"); + is(newFrameDocShellId, frameDocShellId, "check docshell ID remains after reload"); + + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + let entry = shistory.legacySHistory.getEntryAtIndex(shistory.index); + let frameEntry = entry.GetChildAt(0); + is(String(frameEntry.docshellID), frameDocShellId, "check newly added shentry uses the same docshell ID"); + } else { + let p = chromeScript.promiseOneMessage("test1"); + chromeScript.sendAsyncMessage("test1", SpecialPowers.wrap(testWin).browsingContext.id); + let result = await p; + ok(result.success, "legacySHistory worked around ok"); + is(result.value, frameDocShellId, "check newly added shentry uses the same docshell ID"); + } + + webNav.goBack(); + break; + case 2: + ok(e.data.endsWith("file_bug1375833-frame1.html"), "check location"); + is(shistory.count, 4, "check history length"); + is(shistory.index, 2, "check history index"); + + webNav.goBack(); + break; + case 3: + ok(e.data.endsWith("file_bug1375833-frame2.html"), "check location"); + is(shistory.count, 4, "check history length"); + is(shistory.index, 1, "check history index"); + + webNav.goBack(); + break; + case 4: + ok(e.data.endsWith("file_bug1375833-frame1.html"), "check location"); + is(shistory.count, 4, "check history length"); + is(shistory.index, 0, "check history index"); + + if (chromeScript) { + chromeScript.destroy(); + } + testWin.close(); + SimpleTest.finish(); + } + }); + + function getFrameDocShell() { + return SpecialPowers.wrap(testWin.window[0]).docShell; + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1375833">Mozilla Bug 1375833</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1379762.html b/docshell/test/navigation/test_bug1379762.html new file mode 100644 index 0000000000..eda3b539a5 --- /dev/null +++ b/docshell/test/navigation/test_bug1379762.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 1379762</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1379762">Mozilla Bug 1379762</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + /** + * - This page opens new window + * - new window sends 'init' msg + * - onload() in new window sends 'increment_loadCount' msg + * - onpageshow() in new window sends 'increment_testCount' msg + * - This page sends 'forward_back' msg + * - onpageshow() in new window 'increment_testCount' + * - This page sends 'finish_test' msg + * - onpageshow() in new window sends 'finished' msg + */ + var testCount = 0; // Used by the test files. + var loadCount = 0; + var goneBack = false; + var bc = new BroadcastChannel("bug1379762"); + bc.onmessage = (messageEvent) => { + let message = messageEvent.data; + if (message == "init") { + is(testCount, 0, "new window should only be loaded once; otherwise the loadCount variable makes no sense"); + } else if (message == "increment_loadCount") { + loadCount++; + is(loadCount, 1, "Should only get one load") + } else if (message == 'increment_testCount') { + testCount++; + if (testCount == 1) { + bc.postMessage("forward_back"); + goneBack = true; + } else if (testCount == 2) { + ok(goneBack, "We had a chance to navigate backwards and forwards in the new window to test BFCache"); + bc.postMessage("finish_test"); + } + } else if (message == "finished") { + bc.close(); + SimpleTest.finish(); + } + } + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + window.open("file_bug1379762-1.html", "", "width=360,height=480,noopener"); + }); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug13871.html b/docshell/test/navigation/test_bug13871.html new file mode 100644 index 0000000000..0532bc7b56 --- /dev/null +++ b/docshell/test/navigation/test_bug13871.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 50px; } + </style> +<script> +async function runTest() { + navigateByLocation(window0.frames[0]); + navigateByOpen("window1_child0"); + navigateByForm("window2_child0"); + navigateByHyperlink("window3_child0"); + + await waitForFinishedFrames(4); + + isInaccessible(window0.frames[0], "Should not be able to navigate off-domain frame by setting location."); + isInaccessible(window1.frames[0], "Should not be able to navigate off-domain frame by calling window.open."); + isInaccessible(window2.frames[0], "Should not be able to navigate off-domain frame by submitting form."); + isInaccessible(window3.frames[0], "Should not be able to navigate off-domain frame by targeted hyperlink."); + + window0.close(); + window1.close(); + window2.close(); + window3.close(); + + await cleanupWindows(); + SimpleTest.finish(); +} + +// Because our open()'d windows are cross-origin, we can't wait for onload. +// We instead wait for a postMessage from parent.html. +var windows = new Map(); +addEventListener("message", function windowLoaded(evt) { + // Because window.open spins the event loop in order to open new windows, + // we might receive the "ready" message before we call waitForLoad. + // In that case, windows won't contain evt.source and we just note that the + // window is ready. Otherwise, windows contains the "resolve" function for + // that window's promise and we just have to call it. + if (windows.has(evt.source)) { + windows.get(evt.source)(); + } else { + windows.set(evt.source, true); + } +}); + +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var window0 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/parent.html", "window0", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var window1 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/parent.html", "window1", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var window2 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/parent.html", "window2", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var window3 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/parent.html", "window3", "width=10,height=10"); + +function waitForLoad(w) { + return new Promise(function(resolve, reject) { + // If we already got the "ready" message, resolve immediately. + if (windows.has(w)) { + resolve(); + } else { + windows.set(w, resolve); + } + }); +} + +Promise.all([ waitForLoad(window0), + waitForLoad(window1), + waitForLoad(window2), + waitForLoad(window3) ]) + .then(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=13871">Mozilla Bug 13871</a> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug145971.html b/docshell/test/navigation/test_bug145971.html new file mode 100644 index 0000000000..ffad27a9c3 --- /dev/null +++ b/docshell/test/navigation/test_bug145971.html @@ -0,0 +1,29 @@ +<html> + <head> + <script> + let pass = false; + let initialLoad = false; + var bc = new BroadcastChannel("bug145971"); + function checkNavigationTypeEquals2() { + if (performance.navigation.type == 2) { + pass = true; + } + testDone(); + } + + function testDone() { + bc.postMessage({result: pass}); + bc.close(); + window.close(); + } + + function test() { + window.onpageshow = checkNavigationTypeEquals2; + window.location.href = 'goback.html'; + } + </script> + </head> + <body onload="setTimeout(test, 0);"> + Testing bug 145971. + </body> +</html> diff --git a/docshell/test/navigation/test_bug1536471.html b/docshell/test/navigation/test_bug1536471.html new file mode 100644 index 0000000000..f37aedba21 --- /dev/null +++ b/docshell/test/navigation/test_bug1536471.html @@ -0,0 +1,75 @@ + +<!DOCTYPE HTML> +<html> + <!-- + https://bugzilla.mozilla.org/show_bug.cgi?id=1536471 + --> +<head> + <title>Test for Bug 1536471</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript"> + + let testWin; + async function test() { + // Open a new tab and load a document with an iframe inside + testWin = window.open("file_bug1536471.html"); + await waitForLoad(); + var iframe = testWin.document.getElementById("staticFrame"); + is(testWin.history.length, 1, "Checking the number of session history entries when there is only one iframe"); + + // Navigate the iframe to different pages + await loadUriInFrame(iframe, "frame1.html"); + is(testWin.history.length, 2, "Checking the number of session history entries after having navigated a single iframe 1 time"); + await loadUriInFrame(iframe, "frame2.html"); + is(testWin.history.length, 3, "Checking the number of session history entries after having navigated a single iframe 2 times"); + await loadUriInFrame(iframe, "frame3.html"); + is(testWin.history.length, 4, "Checking the number of session history entries after having navigated a single iframe 3 times"); + + // Reload the top document + testWin.location.reload(true); + await waitForLoad(); + is(testWin.history.length, 1, "Checking the number of session history entries after reloading the top document"); + + testWin.close(); + SimpleTest.finish(); + } + + async function waitForLoad() { + await new Promise(resolve => { + window.bodyOnLoad = function() { + setTimeout(resolve, 0); + window.bodyOnLoad = undefined; + }; + }); + } + + async function iframeOnload(frame) { + return new Promise(resolve => { + frame.addEventListener("load", () => { + setTimeout(resolve, 0); + }, {once: true}); + }); + } + + async function loadUriInFrame(frame, uri) { + let onloadPromise = iframeOnload(frame); + frame.src = uri; + await onloadPromise; + } + </script> +</head> + +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1536471">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +<body onload="test()"> +</body> +</html> + diff --git a/docshell/test/navigation/test_bug1583110.html b/docshell/test/navigation/test_bug1583110.html new file mode 100644 index 0000000000..f1c1b65e4d --- /dev/null +++ b/docshell/test/navigation/test_bug1583110.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>test bug 1583110</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + var bc = new BroadcastChannel("bug1583110"); + var pageshowCount = 0; + bc.onmessage = function(event) { + ok(event.data.type == "pageshow"); + ++pageshowCount; + if (pageshowCount == 1) { + bc.postMessage("loadnext"); + } else if (pageshowCount == 2) { + bc.postMessage("back"); + } else { + ok(event.data.persisted, "Should have persisted the first page"); + bc.close(); + SimpleTest.finish(); + } + } + + function test() { + window.open("file_bug1583110.html", "", "noopener"); + } + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1609475.html b/docshell/test/navigation/test_bug1609475.html new file mode 100644 index 0000000000..4dbe7d17d6 --- /dev/null +++ b/docshell/test/navigation/test_bug1609475.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 1609475</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1609475">Mozilla Bug 1609475</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_bug1609475.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + testWindow.close(); + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1699721.html b/docshell/test/navigation/test_bug1699721.html new file mode 100644 index 0000000000..687c5306cf --- /dev/null +++ b/docshell/test/navigation/test_bug1699721.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script type="text/javascript"> + add_task(async function() { + let popup = window.open("blank.html"); + + info("opened popup"); + await new Promise(resolve => { + popup.addEventListener("load", resolve, { once: true }); + }); + + info("popup blank.html loaded"); + let tell_opener = new URL("file_tell_opener.html", location.href); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let xorigin_url = new URL(tell_opener.pathname, "http://example.com"); + + let resolveStartedUnload; + let startedUnload = new Promise(resolve => { + resolveStartedUnload = resolve; + }); + let didFinishUnload = false; + + let finishUnload = false; + popup.addEventListener("unload", function() { + resolveStartedUnload(); + try { + // Spin a nested event loop in unload until we set `finishUnload`. + SpecialPowers.Services.tm.spinEventLoopUntil( + "Test(test_switch_back_nested.html)", () => finishUnload); + } finally { + info("exiting from unload nested event loop..."); + didFinishUnload = true; + } + }); + + info("wait for message from popup"); + let messagePromise = new Promise(resolve => { + addEventListener("message", evt => { + resolve(); + }, { once: true }); + }); + popup.location = xorigin_url.href; + await messagePromise; + + info("popup loaded, ensuring we're in unload"); + await startedUnload; + is(didFinishUnload, false, "unload shouldn't have finished"); + + let switchStarted = SpecialPowers.spawnChrome([], async () => { + await new Promise(resolve => { + async function observer(subject, topic) { + is(topic, "http-on-examine-response"); + + let uri = subject.QueryInterface(Ci.nsIChannel).URI; + if (!uri.filePath.endsWith("file_tell_opener.html")) { + return; + } + + Services.obs.removeObserver(observer, "http-on-examine-response"); + + // spin the event loop a few times to ensure we resolve after the process switch + for (let i = 0; i < 10; ++i) { + await new Promise(res => Services.tm.dispatchToMainThread(res)); + } + + info("resolving!"); + resolve(); + } + Services.obs.addObserver(observer, "http-on-examine-response"); + }); + }); + + info("Navigating back to the current process"); + await SpecialPowers.spawn(popup, [tell_opener.href], (href) => { + content.location.href = href; + }); + + let messagePromise2 = new Promise(resolve => { + addEventListener("message", evt => { + resolve(); + }, { once: true }); + }); + + info("Waiting for the process switch to start"); + await switchStarted; + + // Finish unloading, and wait for the unload to complete + is(didFinishUnload, false, "unload shouldn't be finished"); + finishUnload = true; + await new Promise(resolve => setTimeout(resolve, 0)); + is(didFinishUnload, true, "unload should be finished"); + + info("waiting for navigation to complete"); + await messagePromise2; + + info("closing popup"); + popup.close(); + + ok(true, "Didn't crash"); + }); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1706090.html b/docshell/test/navigation/test_bug1706090.html new file mode 100644 index 0000000000..293148b9c6 --- /dev/null +++ b/docshell/test/navigation/test_bug1706090.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1706090</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + var bc = new BroadcastChannel("bug1706090"); + var pageshowCount = 0; + bc.onmessage = function(event) { + if (event.data.type == "pageshow") { + ++pageshowCount; + if (pageshowCount == 1) { + is(event.data.persisted, false, "Shouldn't have persisted the initial load."); + bc.postMessage("sameOrigin"); + } else if (pageshowCount == 2) { + bc.postMessage("back"); + } else if (pageshowCount == 3) { + is(event.data.persisted, false, "Shouldn't have persisted same origin load."); + bc.postMessage("crossOrigin"); + } else if (pageshowCount == 4) { + is(event.data.persisted, true, "Should have persisted cross origin load."); + bc.postMessage("sameSite"); + } else if (pageshowCount == 5) { + is(event.data.persisted, false, "Shouldn't have persisted same site load."); + bc.postMessage("close"); + } + } else if (event.data == "closed") { + bc.close(); + SimpleTest.finish(); + } + }; + + function runTest() { + SpecialPowers.pushPrefEnv({set: [["docshell.shistory.bfcache.allow_unload_listeners", true]]}, () => { + window.open("file_bug1706090.html", "", "noopener"); + }); + } + </script> +</head> +<body onload="runTest()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1745638.html b/docshell/test/navigation/test_bug1745638.html new file mode 100644 index 0000000000..594c464da3 --- /dev/null +++ b/docshell/test/navigation/test_bug1745638.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>bug 1745638</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + // This test triggers an assertion in the old session history + // implementation. + SimpleTest.expectAssertions(0, 1); + + SimpleTest.waitForExplicitFinish(); + var testWindow; + var loadCount = 0; + function test() { + testWindow = window.open("file_bug1745638.html"); + } + + function pageLoaded() { + ++loadCount; + is(testWindow.document.getElementById('testFrame').contentDocument.body.innerHTML, + "passed", + "Iframe's textual content should be 'passed'."); + if (loadCount == 1) { + testWindow.history.go(0); + } else { + testWindow.close(); + SimpleTest.finish(); + } + } + + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1747019.html b/docshell/test/navigation/test_bug1747019.html new file mode 100644 index 0000000000..c7995737df --- /dev/null +++ b/docshell/test/navigation/test_bug1747019.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test session history and caching</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + var win; + var loadCount = 0; + var initialContent; + // The test loads first a page in a new window, then using + // form submission loads another page and then using form submission + // again loads third page. That page triggers history.go(-1). + // The second page is loaded now again and should have the same content + // as it had before. + function test() { + win = window.open("cache_control_max_age_3600.sjs?initial"); + window.onmessage = (e) => { + is(e.data, "loaded", "Should get load message 'loaded'"); + ++loadCount; + if (loadCount == 1) { + win.document.forms[0].submit(); + } else if (loadCount == 2) { + initialContent = win.document.body.textContent; + info("The initial content is [" + initialContent + "]."); + win.document.forms[0].submit(); + } else if (loadCount == 3) { + let newContent = win.document.body.textContent; + info("The new content is [" + newContent + "]."); + win.close(); + is(initialContent, newContent, "Should have loaded the page from cache."); + SimpleTest.finish(); + } else { + ok(false, "Unexpected load count."); + } + } + } + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1750973.html b/docshell/test/navigation/test_bug1750973.html new file mode 100644 index 0000000000..9f87075b90 --- /dev/null +++ b/docshell/test/navigation/test_bug1750973.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>The layout state restoration when reframing the root element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + function test() { + window.open("file_bug1750973.html"); + } + </script> +</head> +<body onload="setTimeout(test)"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug1758664.html b/docshell/test/navigation/test_bug1758664.html new file mode 100644 index 0000000000..662242e44a --- /dev/null +++ b/docshell/test/navigation/test_bug1758664.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1758664</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + function test() { + window.open("file_bug1758664.html"); + } + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug270414.html b/docshell/test/navigation/test_bug270414.html new file mode 100644 index 0000000000..0635e32888 --- /dev/null +++ b/docshell/test/navigation/test_bug270414.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 50px; } + </style> +<script> +/* eslint-disable no-useless-concat */ +/* global window0:true, window1:true, window2:true, window3:true */ +var headerHTML = "<html><head>" + + "<script src='/tests/SimpleTest/EventUtils.js'><\/script>" + + "<script src='NavigationUtils.js'><\/script>" + + "</head><body>"; +var footerHTML = "</body></html>"; + +function testChild0() { + if (!window.window0) { + window0 = window.open("", "window0", "width=10,height=10"); + window0.document.open(); + // eslint-disable-next-line no-unsanitized/method + window0.document.write(headerHTML); + window0.document.write("<script>navigateByLocation(opener.frames[0])<\/script>"); + // eslint-disable-next-line no-unsanitized/method + window0.document.write(footerHTML); + window0.document.close(); + } +} + +function testChild1() { + if (!window.window1) { + window1 = window.open("", "window1", "width=10,height=10"); + window1.document.open(); + // eslint-disable-next-line no-unsanitized/method + window1.document.write(headerHTML); + window1.document.write("<script>navigateByOpen('child1');<\/script>"); + // eslint-disable-next-line no-unsanitized/method + window1.document.write(footerHTML); + window1.document.close(); + } +} + +function testChild2() { + if (!window.window2) { + window2 = window.open("", "window2", "width=10,height=10"); + window2.document.open(); + // eslint-disable-next-line no-unsanitized/method + window2.document.write(headerHTML); + window2.document.write("<script>navigateByForm('child2');<\/script>"); + // eslint-disable-next-line no-unsanitized/method + window2.document.write(footerHTML); + window2.document.close(); + } +} + +function testChild3() { + if (!window.window3) { + window3 = window.open("", "window3", "width=10,height=10"); + window3.document.open(); + // eslint-disable-next-line no-unsanitized/method + window3.document.write(headerHTML); + window3.document.write("<script>navigateByHyperlink('child3');<\/script>"); + // eslint-disable-next-line no-unsanitized/method + window3.document.write(footerHTML); + window3.document.close(); + } +} + +add_task(async function() { + await waitForFinishedFrames(4); + + await isNavigated(frames[0], "Should be able to navigate on-domain opener's children by setting location."); + await isNavigated(frames[1], "Should be able to navigate on-domain opener's children by calling window.open."); + await isNavigated(frames[2], "Should be able to navigate on-domain opener's children by submitting form."); + await isNavigated(frames[3], "Should be able to navigate on-domain opener's children by targeted hyperlink."); + + window0.close(); + window1.close(); + window2.close(); + window3.close(); + + await cleanupWindows(); +}); + +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=270414">Mozilla Bug 270414</a> +<div id="frames"> +<iframe onload="testChild0();" name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe onload="testChild1();" name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe onload="testChild2();" name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe onload="testChild3();" name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +</div> +<pre id="test"> +<script type="text/javascript"> +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug278916.html b/docshell/test/navigation/test_bug278916.html new file mode 100644 index 0000000000..9e2335721e --- /dev/null +++ b/docshell/test/navigation/test_bug278916.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> +<script> +window.onload = async function() { + document.getElementById("link0").href = target_url; + sendMouseEvent({type: "click"}, "link0"); + + await waitForFinishedFrames(1); + + var array_of_frames = await getFramesByName("window0"); + is(array_of_frames.length, 1, "Should only open one window using a fancy hyperlink."); + + for (var i = 0; i < array_of_frames.length; ++i) + array_of_frames[i].close(); + + await cleanupWindows(); + SimpleTest.finish(); +}; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=278916">Mozilla Bug 278916</a> +<div id="links"> +<a id="link0" target="window0" onclick="window.open('', 'window0', 'width=10,height=10');">This is a fancy hyperlink</a> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug279495.html b/docshell/test/navigation/test_bug279495.html new file mode 100644 index 0000000000..245ed14ed4 --- /dev/null +++ b/docshell/test/navigation/test_bug279495.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> +<script> +window.onload = async function() { + document.getElementById("link0").href = target_url; + document.getElementById("link1").href = target_url; + + sendMouseEvent({type: "click"}, "link0"); + sendMouseEvent({type: "click"}, "link1"); + + await waitForFinishedFrames(2); + await countAndClose("window0", 1); + await countAndClose("window1", 1); + + await cleanupWindows(); + SimpleTest.finish(); +}; + +async function countAndClose(name, expected_count) { + var array_of_frames = await getFramesByName(name); + is(array_of_frames.length, expected_count, + "Should only open " + expected_count + + " window(s) with name " + name + " using a fancy hyperlink."); +} +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=279495">Mozilla Bug 279495</a> +<div id="links"> +<a id="link0" target="window0" onclick="window.open('blank.html', 'window0', 'width=10,height=10');">This is a fancy hyperlink</a> +<a id="link1" target="window1" onclick="window.open('https://test1.example.org/tests/docshell/test/navigation/blank.html', 'window1', 'width=10,height=10');">This is a fancy hyperlink</a> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug344861.html b/docshell/test/navigation/test_bug344861.html new file mode 100644 index 0000000000..5ab8809d27 --- /dev/null +++ b/docshell/test/navigation/test_bug344861.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=344861 +--> +<head> + <title>Test for Bug 344861</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=344861">Mozilla Bug 344861</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 344861 */ +SimpleTest.waitForExplicitFinish(); + +var newwindow = window.open("/", "testwindow", "width=200,height=200"); +newwindow.onload = function() { + is(newwindow.innerHeight, 200, "window.open has correct height dimensions"); + is(newwindow.innerWidth, 200, "window.open has correct width dimensions"); + SimpleTest.finish(); + newwindow.close(); +}; +</script> +</pre> +</body> +</html> + + diff --git a/docshell/test/navigation/test_bug386782.html b/docshell/test/navigation/test_bug386782.html new file mode 100644 index 0000000000..f6ad85f5a6 --- /dev/null +++ b/docshell/test/navigation/test_bug386782.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=386782 +--> +<head> + <title>Test for Bug 386782</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <script> + + // This tests if we can load a document whose root is in designMode, + // edit it, navigate to a new page, navigate back, still edit, and still + // undo/redo. Note that this is different from the case where the + // designMode document is in a frame inside the window, as this means + // the editable region is not in the root docshell (a less complicated case). + + var gTests = [ + { + // <html><body><p>designModeDocument</p></body></html> + url: "file_bug386782_designmode.html", + name: "designModeNavigate", + onload(doc) { doc.designMode = "on"; }, + expectedBodyBeforeEdit: "<p>designModeDocument</p>", + expectedBodyAfterEdit: "<p>EDITED designModeDocument</p>", + expectedBodyAfterSecondEdit: "<p>EDITED TWICE designModeDocument</p>", + }, + { + // <html><body contentEditable="true"><p>contentEditable</p></body></html> + url: "file_bug386782_contenteditable.html", + name: "contentEditableNavigate", + expectedBodyBeforeEdit: "<p>contentEditable</p>", + expectedBodyAfterEdit: "EDITED <br><p>contentEditable</p>", + expectedBodyAfterSecondEdit: "EDITED TWICE <br><p>contentEditable</p>", + }, + ]; + + var gTest = null; + + add_task(async () => { + while (gTests.length) { + gTest = gTests.shift(); + await runTest(); + } + }); + + async function runTest() { + gTest.window = window.open(gTest.url, gTest.name, "width=500,height=500"); + let e = await new Promise(r => window.onmessage = r); + is(e.data.persisted, false, "Initial load cannot be persisted"); + if ("onload" in gTest) { + gTest.onload(gTest.window.document); + } + await SimpleTest.promiseFocus(gTest.window); + + gTest.window.document.body.focus(); + + // WARNING: If the following test fails, give the setTimeout() in the onload() + // a bit longer; the doc hasn't had enough time to setup its editor. + is(gTest.window.document.body.innerHTML, gTest.expectedBodyBeforeEdit, "Is doc setup yet"); + sendString("EDITED ", gTest.window); + is(gTest.window.document.body.innerHTML, gTest.expectedBodyAfterEdit, "Editing failed."); + + gTest.window.location = "about:blank"; + await new Promise(r => gTest.window.onpagehide = r); + // The active document is updated synchronously after "pagehide" (and + // its associated microtasks), so, after waiting for the next global + // task, gTest.window will be proxying the realm associated with the + // "about:blank" document. + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#update-the-session-history-with-the-new-page + await new Promise(r => setTimeout(r)); + is(gTest.window.location.href, "about:blank", "location.href"); + await SimpleTest.promiseFocus(gTest.window, true); + + gTest.window.history.back(); + e = await new Promise(r => window.onmessage = r); + // Skip the test if the page is not loaded from the bf-cache when going back. + if (e.data.persisted) { + checkStillEditable(); + } + gTest.window.close(); + } + + function checkStillEditable() { + // Check that the contents are correct. + is(gTest.window.document.body.innerHTML, gTest.expectedBodyAfterEdit, "Edited contents still correct?"); + + // Check that we can undo/redo and the contents are correct. + gTest.window.document.execCommand("undo", false, null); + is(gTest.window.document.body.innerHTML, gTest.expectedBodyBeforeEdit, "Can we undo?"); + + gTest.window.document.execCommand("redo", false, null); + is(gTest.window.document.body.innerHTML, gTest.expectedBodyAfterEdit, "Can we redo?"); + + // Check that we can still edit the page. + gTest.window.document.body.focus(); + sendString("TWICE ", gTest.window); + is(gTest.window.document.body.innerHTML, gTest.expectedBodyAfterSecondEdit, "Can we still edit?"); + } + + </script> + +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=386782">Mozilla Bug 386782</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 386782 */ + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_bug430624.html b/docshell/test/navigation/test_bug430624.html new file mode 100644 index 0000000000..49afb023b9 --- /dev/null +++ b/docshell/test/navigation/test_bug430624.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=430624 +--> +<head> + <title>Test for Bug 430624</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=430624">Mozilla Bug 430624</a> +<p id="display"></p> + + + +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 430624 */ + +function onLoad() { + window.frames[0].frameElement.onload = onReload; + // eslint-disable-next-line no-self-assign + window.frames[0].frameElement.srcdoc = window.frames[0].frameElement.srcdoc; +} + +function onReload() { + var iframe = window.frames[0].frameElement; + SimpleTest.waitForFocus(doTest, iframe.contentWindow); + iframe.contentDocument.body.focus(); +} + +function doTest() { + var bodyElement = window.frames[0].frameElement.contentDocument.body; + bodyElement.focus(); + sendString("Still ", window.frames[0].frameElement.contentWindow); + + is(bodyElement.innerHTML, "Still contentEditable", "Check we're contentEditable after reload"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> + +<iframe onload="onLoad()" srcdoc="<body contenteditable>contentEditable</body>"></iframe> + +</body> +</html> + diff --git a/docshell/test/navigation/test_bug430723.html b/docshell/test/navigation/test_bug430723.html new file mode 100644 index 0000000000..dd85c7fb08 --- /dev/null +++ b/docshell/test/navigation/test_bug430723.html @@ -0,0 +1,124 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=430723 +--> +<head> + <title>Test for Bug 430723</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=430723">Mozilla Bug 430723</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +// <![CDATA[ + +/** Test for Bug 430723 */ + +var BASE_URI = "http://mochi.test:8888/tests/docshell/test/navigation/"; +var gTallRedBoxURI = BASE_URI + "redbox_bug430723.html"; +var gTallBlueBoxURI = BASE_URI + "bluebox_bug430723.html"; + +window.onload = runTest; + +var testWindow; +var testNum = 0; + +var smoothScrollPref = "general.smoothScroll"; +function runTest() { + SpecialPowers.pushPrefEnv({"set": [[smoothScrollPref, false]]}, function() { + testWindow = window.open(gTallRedBoxURI, "testWindow", "width=300,height=300,location=yes,scrollbars=yes"); + }); +} + +var nextTest = function() { + testNum++; + switch (testNum) { + case 1: setTimeout(step1, 0); break; + case 2: setTimeout(step2, 0); break; + case 3: setTimeout(step3, 0); break; + } +}; + +var step1 = function() { + window.is(String(testWindow.location), gTallRedBoxURI, "Ensure red page loaded."); + + // Navigate down and up. + is(testWindow.document.body.scrollTop, 0, + "Page1: Ensure the scrollpane is at the top before we start scrolling."); + testWindow.addEventListener("scroll", function() { + isnot(testWindow.document.body.scrollTop, 0, + "Page1: Ensure we can scroll down."); + SimpleTest.executeSoon(step1_2); + }, {capture: true, once: true}); + sendKey("DOWN", testWindow); + + function step1_2() { + testWindow.addEventListener("scroll", function() { + is(testWindow.document.body.scrollTop, 0, + "Page1: Ensure we can scroll up, back to the top."); + + // Nav to blue box page. This should fire step2. + testWindow.location = gTallBlueBoxURI; + }, {capture: true, once: true}); + sendKey("UP", testWindow); + } +}; + + +var step2 = function() { + window.is(String(testWindow.location), gTallBlueBoxURI, "Ensure blue page loaded."); + + // Scroll around a bit. + is(testWindow.document.body.scrollTop, 0, + "Page2: Ensure the scrollpane is at the top before we start scrolling."); + + var scrollTest = function() { + if (++count < 2) { + SimpleTest.executeSoon(function() { sendKey("DOWN", testWindow); }); + } else { + testWindow.removeEventListener("scroll", scrollTest, true); + + isnot(testWindow.document.body.scrollTop, 0, + "Page2: Ensure we could scroll."); + + // Navigate backwards. This should fire step3. + testWindow.history.back(); + } + }; + + var count = 0; + testWindow.addEventListener("scroll", scrollTest, true); + sendKey("DOWN", testWindow); +}; + +var step3 = function() { + window.is(String(testWindow.location), gTallRedBoxURI, + "Ensure red page restored from history."); + + // Check we can still scroll with the keys. + is(testWindow.document.body.scrollTop, 0, + "Page1Again: Ensure scroll pane at top before we scroll."); + testWindow.addEventListener("scroll", function() { + isnot(testWindow.document.body.scrollTop, 0, + "Page2Again: Ensure we can still scroll."); + + testWindow.close(); + window.SimpleTest.finish(); + }, {capture: true, once: true}); + sendKey("DOWN", testWindow); +}; + +SimpleTest.waitForExplicitFinish(); + +// ]]> +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_child.html b/docshell/test/navigation/test_child.html new file mode 100644 index 0000000000..87237471cd --- /dev/null +++ b/docshell/test/navigation/test_child.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 50px; } + </style> +<script> +if (!navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 1); +} + +window.onload = async function() { + navigateByLocation(frames[0]); + navigateByOpen("child1"); + navigateByForm("child2"); + navigateByHyperlink("child3"); + + await waitForFinishedFrames(4); + await isNavigated(frames[0], "Should be able to navigate off-domain child by setting location."); + await isNavigated(frames[1], "Should be able to navigate off-domain child by calling window.open."); + await isNavigated(frames[2], "Should be able to navigate off-domain child by submitting form."); + await isNavigated(frames[3], "Should be able to navigate off-domain child by targeted hyperlink."); + + await cleanupWindows(); + SimpleTest.finish(); +}; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a> +<div id="frames"> +<iframe name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_contentpolicy_block_window.html b/docshell/test/navigation/test_contentpolicy_block_window.html new file mode 100644 index 0000000000..7ce337c131 --- /dev/null +++ b/docshell/test/navigation/test_contentpolicy_block_window.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1329288 +--> +<head> + <title>Test for Bug 1329288</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1329288">Mozilla Bug 1329288</a> + + +<!-- have a testlink which we can use for the test to open a new window --> +<a href="http://test1.example.org/tests/docshell/test/navigation/file_contentpolicy_block_window.html" + target="_blank" + id="testlink">This is a link</a> + +<script class="testbody" type="text/javascript"> +/* + * Description of the test: + * The test tries to open a new window and makes sure that a registered contentPolicy + * gets called with the right (a non null) 'context' for the TYPE_DOCUMENT load. + */ + +const Ci = SpecialPowers.Ci; + +var categoryManager = SpecialPowers.Services.catMan; +var componentManager = SpecialPowers.Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); + +// Content policy / factory implementation for the test +var policyID = SpecialPowers.wrap(SpecialPowers.Components).ID("{b80e19d0-878f-d41b-2654-194714a4115c}"); +var policyName = "@mozilla.org/testpolicy;1"; +var policy = { + // nsISupports implementation + // eslint-disable-next-line mozilla/use-chromeutils-generateqi + QueryInterface(iid) { + iid = SpecialPowers.wrap(iid); + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIFactory) || + iid.equals(Ci.nsIContentPolicy)) + return this; + throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE; + }, + + // nsIFactory implementation + createInstance(iid) { + return this.QueryInterface(iid); + }, + + // nsIContentPolicy implementation + shouldLoad(contentLocation, loadInfo, mimeTypeGuess) { + let contentType = loadInfo.externalContentPolicyType; + let context = loadInfo.loadingContext; + + if (SpecialPowers.wrap(contentLocation).spec !== document.getElementById("testlink").href) { + // not the URI we are looking for, allow the load + return Ci.nsIContentPolicy.ACCEPT; + } + + is(contentType, Ci.nsIContentPolicy.TYPE_DOCUMENT, + "needs to be type document load"); + ok(context, "context is not allowed to be null"); + ok(context.name.endsWith("test_contentpolicy_block_window.html"), + "context should be the current window"); + + // remove the policy and finish test. + categoryManager.deleteCategoryEntry("content-policy", policyName, false); + + setTimeout(function() { + // Component must be unregistered delayed, otherwise other content + // policy will not be removed from the category correctly + componentManager.unregisterFactory(policyID, policy); + }, 0); + + SimpleTest.finish(); + return Ci.nsIContentPolicy.REJECT_REQUEST; + }, + + shouldProcess(contentLocation, loadInfo, mimeTypeGuess) { + return Ci.nsIContentPolicy.ACCEPT; + }, +}; + +policy = SpecialPowers.wrapCallbackObject(policy); +componentManager.registerFactory(policyID, "Test content policy", policyName, policy); +categoryManager.addCategoryEntry("content-policy", policyName, policyName, false, true); + +SimpleTest.waitForExplicitFinish(); + +// now everything is set up, let's start the test +document.getElementById("testlink").click(); + +</script> +</body> +</html> diff --git a/docshell/test/navigation/test_docshell_gotoindex.html b/docshell/test/navigation/test_docshell_gotoindex.html new file mode 100644 index 0000000000..992c9c9dbe --- /dev/null +++ b/docshell/test/navigation/test_docshell_gotoindex.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1684310</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + function test() { + /* + * This test is for nsIWebNavigation.gotoIndex. + * + * The test + * - opens a new window + * - loads a page there + * - loads another page + * - navigates to some fragments in the page + * - goes back to one of the fragments + * - tries to go back to the initial page. + */ + window.open("file_docshell_gotoindex.html"); + } + </script> +</head> +<body onload="test()"> +<p id="display"></p> +</body> +</html> diff --git a/docshell/test/navigation/test_dynamic_frame_forward_back.html b/docshell/test/navigation/test_dynamic_frame_forward_back.html new file mode 100644 index 0000000000..f3a349e09a --- /dev/null +++ b/docshell/test/navigation/test_dynamic_frame_forward_back.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 508537</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=508537">Mozilla Bug 508537</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_bug508537_1.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + testWindow.close(); + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_evict_from_bfcache.html b/docshell/test/navigation/test_evict_from_bfcache.html new file mode 100644 index 0000000000..0b1eb2fca4 --- /dev/null +++ b/docshell/test/navigation/test_evict_from_bfcache.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Evict a page from bfcache</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + /* + * This test checks that a page can be evicted from bfcache. Sending a + * message to an open BroadcastChannel is used for this. + * + * First the test opens a window and loads a page there. Another page is then + * loaded and then session history is navigated back to check if bfcache is + * enabled. If not, close message is sent to close the opened window and this + * controller page will finish the test. + * If bfcache is enabled, session history goes forward, but the + * BroadcastChannel in the page isn't closed. Then sending the message to go + * back again should evict the bfcached page. + * Close message is sent and window closed and test finishes. + */ + + SimpleTest.waitForExplicitFinish(); + var bc = new BroadcastChannel("evict_from_bfcache"); + var pageshowCount = 0; + bc.onmessage = function(event) { + if (event.data.type == "pageshow") { + ++pageshowCount; + info("pageshow " + pageshowCount); + if (pageshowCount == 1) { + bc.postMessage("nextpage"); + } else if (pageshowCount == 2) { + bc.postMessage("back"); + } else if (pageshowCount == 3) { + if (!event.data.persisted) { + ok(true, "BFCache isn't enabled."); + bc.postMessage("close"); + } else { + bc.postMessage("forward"); + } + } else if (pageshowCount == 4) { + bc.postMessage("back"); + } else if (pageshowCount == 5) { + ok(!event.data.persisted, + "The page should have been evicted from BFCache"); + bc.postMessage("close"); + } + } else if (event.data == "closed") { + SimpleTest.finish(); + } + } + + function runTest() { + window.open("file_evict_from_bfcache.html", "", "noopener"); + } + </script> +</head> +<body onload="runTest()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_fragment_handling_during_load.html b/docshell/test/navigation/test_fragment_handling_during_load.html new file mode 100644 index 0000000000..9c082c2ecf --- /dev/null +++ b/docshell/test/navigation/test_fragment_handling_during_load.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for fragment navigation during load</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=978408">Mozilla Bug 978408</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_fragment_handling_during_load.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + testWindow.close(); + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_grandchild.html b/docshell/test/navigation/test_grandchild.html new file mode 100644 index 0000000000..10cf610664 --- /dev/null +++ b/docshell/test/navigation/test_grandchild.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 200px; } + </style> +<script> +if (!navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 1); +} + +window.onload = async function() { + navigateByLocation(frames[0].frames[0]); + navigateByOpen("child1_child0"); + navigateByForm("child2_child0"); + navigateByHyperlink("child3_child0"); + + await waitForFinishedFrames(4); + await isNavigated(frames[0].frames[0], "Should be able to navigate off-domain grandchild by setting location."); + await isNavigated(frames[1].frames[0], "Should be able to navigate off-domain grandchild by calling window.open."); + await isNavigated(frames[2].frames[0], "Should be able to navigate off-domain grandchild by submitting form."); + await isNavigated(frames[3].frames[0], "Should be able to navigate off-domain grandchild by targeted hyperlink."); + + await cleanupWindows(); + SimpleTest.finish(); +}; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a> +<div id="frames"> +<iframe name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/parent.html"></iframe> +<iframe name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/parent.html"></iframe> +<iframe name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/parent.html"></iframe> +<iframe name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/parent.html"></iframe> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_load_history_entry.html b/docshell/test/navigation/test_load_history_entry.html new file mode 100644 index 0000000000..8ca3fcb913 --- /dev/null +++ b/docshell/test/navigation/test_load_history_entry.html @@ -0,0 +1,196 @@ + +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="/tests/SimpleTest/SpecialPowers.js"></script> + <script type="application/javascript"> + /* + * Perform the following steps. + * 1) Go to file_load_history_entry_page_with_two_links.html, which contains two links, 'link1' and 'link2' + * 2) Click on 'link1' to be taken to file_load_history_entry_page_with_two_links.html#1 + * 3) Click on 'link2' to be taken to file_load_history_entry_page_with_two_links.html#2 + * 4) Go to file_load_history_entry_page_with_one_link.html + * 5) Push state to go to file_load_history_entry_page_with_one_link.html#1 + * + * After each step + * - Check the number of session history entries + * - Reload the document and do the above again + * - Navigate back and check the correct history index + * - Navigate forward and check the correct history index and location + */ + async function test() { + let testWin; + var promise; + var previousLocation; + var numSHEntries = 0; + + // Step 1. Open a new tab and load a document with two links inside + // Now we are at file_load_history_entry_page_with_two_links.html + numSHEntries++; + promise = waitForLoad(); + testWin = window.open("file_load_history_entry_page_with_two_links.html"); + await promise; + + let shistory = SpecialPowers.wrap(testWin) + .docShell + .QueryInterface(SpecialPowers.Ci.nsIWebNavigation) + .sessionHistory; + + // Step 2. Navigate the document by clicking on the 1st link + // Now we are at file_load_history_entry_page_with_two_links.html#1 + numSHEntries++; + previousLocation = testWin.location.href; + await clickLink(testWin, "link1"); + await doAfterEachTest(testWin, shistory, numSHEntries, previousLocation); + + // Step 3. Navigate the document by clicking the 2nd link + // Now we are file_load_history_entry_page_with_two_links.html#2 + numSHEntries++; + previousLocation = testWin.location.href; + await clickLink(testWin, "link2"); + await doAfterEachTest(testWin, shistory, numSHEntries, previousLocation); + + // Step 4. Navigate the document to a different page + // Now we are at file_load_history_entry_page_with_one_link.html + numSHEntries++; + previousLocation = testWin.location.href; + promise = waitForLoad(); + testWin.location = "file_load_history_entry_page_with_one_link.html"; + await promise; + await doAfterEachTest(testWin, shistory, numSHEntries, previousLocation, + true /* isCrossDocumentLoad */, false /* hashChangeExpected */); + + // Step 5. Push some state + // Now we are at file_load_history_entry_page_with_one_link.html#1 + numSHEntries++; + previousLocation = testWin.location.href; + testWin.history.pushState({foo: "bar"}, "", "#1"); + is(testWin.history.length, numSHEntries, "Session history's length is correct after pushing state"); + is(shistory.index, numSHEntries - 1 /* we haven't switched to new history entry yet*/, + "Session history's index is correct after pushing state"); + await doAfterEachTest(testWin, shistory, numSHEntries, previousLocation); + + // We are done with the test + testWin.close(); + SimpleTest.finish(); + } + + /* + * @prevLocation + * if undefined, it is because there is no page to go back to + * + * @isCrossDocumentLoad + * did we just open a different document + * @hashChangeExpected + * Would we get a hash change event if we navigated backwards and forwards in history? + * This is framed with respect to the previous step, e.g. in the previous step was the + * hash different from the location we have navigated to just before calling this function? + * When we navigate forwards or backwards, we need to wait for this event + * because clickLink() also waits for hashchange event and + * if this function gets called before clickLink(), sometimes hashchange + * events from this function will leak to clickLink. + */ + async function doAfterEachTest(testWin, shistory, expectedNumSHEntries, prevLocation, + isCrossDocumentLoad = false, hashChangeExpected = true) { + var initialLocation = testWin.location.href; + var initialSHIndex = shistory.index; + var promise; + is(testWin.history.length, expectedNumSHEntries, "Session history's length is correct"); + + // Reload the document + promise = waitForLoad(); + testWin.location.reload(true); + await promise; + is(testWin.history.length, expectedNumSHEntries, "Session history's length is correct after reloading"); + + if (prevLocation == undefined) { + return; + } + + var hashChangePromise; + if (hashChangeExpected) { + hashChangePromise = new Promise(resolve => { + testWin.addEventListener("hashchange", resolve, {once: true}); + }); + } + // Navigate backwards + if (isCrossDocumentLoad) { + // Current page must have been a cross document load, so we just need to wait for + // document load to complete after we navigate the history back + // because popstate event will not be fired in this case + promise = waitForLoad(); + } else { + promise = waitForPopstate(testWin); + } + testWin.history.back(); + await promise; + if (hashChangeExpected) { + await hashChangePromise; + } + is(testWin.location.href, prevLocation, "Window location is correct after navigating back in history"); + is(shistory.index, initialSHIndex - 1, "Session history's index is correct after navigating back in history"); + + // Navigate forwards + if (isCrossDocumentLoad) { + promise = waitForLoad(); + } else { + promise = waitForPopstate(testWin); + } + if (hashChangeExpected) { + hashChangePromise = new Promise(resolve => { + testWin.addEventListener("hashchange", resolve, {once: true}); + }); + } + testWin.history.forward(); + await promise; + if (hashChangeExpected) { + await hashChangePromise; + } + is(testWin.location.href, initialLocation, "Window location is correct after navigating forward in history"); + is(shistory.index, initialSHIndex, "Session history's index is correct after navigating forward in history"); + } + + async function waitForLoad() { + return new Promise(resolve => { + window.bodyOnLoad = function() { + setTimeout(resolve, 0); + window.bodyOnLoad = undefined; + }; + }); + } + + async function waitForPopstate(win) { + return new Promise(resolve => { + win.addEventListener("popstate", (e) => { + setTimeout(resolve, 0); + }, {once: true}); + }); + } + + async function clickLink(win, id) { + var link = win.document.getElementById(id); + let clickPromise = new Promise(resolve => { + win.addEventListener("hashchange", resolve, {once: true}); + }); + link.click(); + await clickPromise; + } + + </script> +</head> + +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1539482">Bug 1539482</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +<body onload="test()"> +</body> +</html> + diff --git a/docshell/test/navigation/test_meta_refresh.html b/docshell/test/navigation/test_meta_refresh.html new file mode 100644 index 0000000000..bda9a9fe73 --- /dev/null +++ b/docshell/test/navigation/test_meta_refresh.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test meta refresh</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + let hasLoadedInitialOnce = false; + let bc = new BroadcastChannel("test_meta_refresh"); + bc.onmessage = function(event) { + info(event.data.load || event.data); + if (event.data.load == "initial") { + if (!hasLoadedInitialOnce) { + hasLoadedInitialOnce = true; + bc.postMessage("loadnext"); + } else { + bc.postMessage("ensuremetarefresh"); + } + } else if (event.data.load == "nextpage") { + bc.postMessage("back"); + } else if (event.data.load == "refresh") { + bc.postMessage("close"); + } else if (event.data == "closed") { + ok(true, "Meta refresh page was loaded."); + SimpleTest.finish(); + } + } + + function test() { + window.open("file_meta_refresh.html?initial", "", "noopener"); + } + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_navigation_type.html b/docshell/test/navigation/test_navigation_type.html new file mode 100644 index 0000000000..75ea88bcbd --- /dev/null +++ b/docshell/test/navigation/test_navigation_type.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>performance.navigation.type</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + let bc = new BroadcastChannel("navigation_type"); + let pageshowCount = 0; + bc.onmessage = function(event) { + if (event.data == "closed") { + bc.close(); + SimpleTest.finish(); + return; + } + + ++pageshowCount; + if (pageshowCount == 1) { + is(event.data.navigationType, PerformanceNavigation.TYPE_NAVIGATE, + "Should have navigation type TYPE_NAVIGATE."); + bc.postMessage("loadNewPage"); + } else if (pageshowCount == 2) { + is(event.data.navigationType, PerformanceNavigation.TYPE_NAVIGATE, + "Should have navigation type TYPE_NAVIGATE."); + bc.postMessage("back"); + } else if (pageshowCount == 3) { + is(event.data.navigationType, PerformanceNavigation.TYPE_BACK_FORWARD , + "Should have navigation type TYPE_BACK_FORWARD ."); + bc.postMessage("close"); + } else { + ok(false, "Unexpected load"); + } + } + + function test() { + window.open("file_navigation_type.html", "", "noopener"); + } + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_nested_frames.html b/docshell/test/navigation/test_nested_frames.html new file mode 100644 index 0000000000..c3b49e0e23 --- /dev/null +++ b/docshell/test/navigation/test_nested_frames.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 1090918</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1090918">Mozilla Bug 1090918</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_nested_frames.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + testWindow.close() + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_new_shentry_during_history_navigation.html b/docshell/test/navigation/test_new_shentry_during_history_navigation.html new file mode 100644 index 0000000000..0c9adc5280 --- /dev/null +++ b/docshell/test/navigation/test_new_shentry_during_history_navigation.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test adding new session history entries while navigating to another one</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + var win; + + function waitForMessage(msg) { + return new Promise( + function(resolve) { + window.addEventListener("message", + function(event) { + is(event.data, msg, "Got the expected message " + msg); + resolve(); + }, { once: true } + ); + } + ); + } + + async function test() { + + let loadPromise = waitForMessage("load"); + win = window.open("file_new_shentry_during_history_navigation_1.html"); + await loadPromise; + + loadPromise = waitForMessage("load"); + win.location.href = "file_new_shentry_during_history_navigation_2.html"; + await loadPromise; + + let beforeunloadPromise = waitForMessage("beforeunload"); + win.history.back(); + await beforeunloadPromise; + await waitForMessage("load"); + + win.history.back(); + SimpleTest.requestFlakyTimeout("Test that history.back() does not work on the initial entry."); + setTimeout(function() { + win.onmessage = null; + win.close(); + testBfcache(); + }, 500); + window.onmessage = function(event) { + ok(false, "Should not get a message " + event.data); + } + } + + async function testBfcache() { + let bc = new BroadcastChannel("new_shentry_during_history_navigation"); + let pageshowCount = 0; + bc.onmessage = function(event) { + if (event.data.type == "pageshow") { + ++pageshowCount; + info("pageshow: " + pageshowCount + ", page: " + event.data.page); + if (pageshowCount == 1) { + ok(!event.data.persisted, "The page should not be bfcached."); + bc.postMessage("loadnext"); + } else if (pageshowCount == 2) { + ok(!event.data.persisted, "The page should not be bfcached."); + bc.postMessage("loadnext"); + } else if (pageshowCount == 3) { + ok(!event.data.persisted, "The page should not be bfcached."); + bc.postMessage("back"); + } else if (pageshowCount == 4) { + ok(event.data.persisted, "The page should be bfcached."); + bc.postMessage("forward"); + } else if (pageshowCount == 5) { + ok(event.data.page.includes("v2"), "Should have gone forward."); + bc.postMessage("close"); + SimpleTest.finish(); + } + } + }; + win = window.open("file_new_shentry_during_history_navigation_3.html", "", "noopener"); + + } + + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_not-opener.html b/docshell/test/navigation/test_not-opener.html new file mode 100644 index 0000000000..acdb9473e6 --- /dev/null +++ b/docshell/test/navigation/test_not-opener.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 50px; } + </style> +<script> +if (!navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 1); +} + +window.onload = async function() { + // navigateByLocation(window0); // Don't have a handle to the window. + navigateByOpen("window1"); + navigateByForm("window2"); + navigateByHyperlink("window3"); + + await waitForFinishedFrames(6); + + is((await getFramesByName("window1")).length, 2, "Should not be able to navigate popup's popup by calling window.open."); + is((await getFramesByName("window2")).length, 2, "Should not be able to navigate popup's popup by submitting form."); + is((await getFramesByName("window3")).length, 2, "Should not be able to navigate popup's popup by targeted hyperlink."); + + // opener0.close(); + opener1.close(); + opener2.close(); + opener3.close(); + + info("here") + await cleanupWindows(); + info("there") + SimpleTest.finish(); +}; + +// opener0 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/open.html#window0", "_blank", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +let opener1 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/open.html#window1", "_blank", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +let opener2 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/open.html#window2", "_blank", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +let opener3 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/open.html#window3", "_blank", "width=10,height=10"); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_online_offline_bfcache.html b/docshell/test/navigation/test_online_offline_bfcache.html new file mode 100644 index 0000000000..4ad90fd52e --- /dev/null +++ b/docshell/test/navigation/test_online_offline_bfcache.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Online/Offline with BFCache</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + + /* + * The test is designed to work with and without bfcache. + * (1) First the test opens a window which then loads another page which + * goes back to the original page to detect if bfcache is enabled. If + * bfcache isn't enabled, close message is sent to the opened window and it + * closes itself and sends a message back and the test finishes. + * (2) The browser is set to offline mode. The opened page sends message + * that it has received offline event. This controller page then asks the + * page to go forward. The page which comes out from the bfcache gets + * offline event and sends message about that to this controller. + * (3) Browser is set to online mode. Similar cycle as with offline happens. + * (4) Controller page sends close message to the opened window and it + * closes itself and sends a message back and the test finishes. + */ + + function offlineOnline(online) { + function offlineFn() { + /* eslint-env mozilla/chrome-script */ + Services.io.offline = true; + } + function onlineFn() { + /* eslint-env mozilla/chrome-script */ + Services.io.offline = false; + } + SpecialPowers.loadChromeScript(online ? onlineFn : offlineFn); + } + + var bc = new BroadcastChannel("online_offline_bfcache"); + var pageshowCount = 0; + var offlineCount = 0; + var onlineCount = 0; + + bc.onmessage = function(event) { + if (event.data.event == "pageshow") { + ++pageshowCount; + info("pageshow " + pageshowCount); + if (pageshowCount == 1) { + ok(!event.data.persisted); + bc.postMessage("nextpage"); + } else if (pageshowCount == 2) { + ok(!event.data.persisted); + bc.postMessage("back"); + } else if (pageshowCount == 3) { + if (!event.data.persisted) { + info("BFCache is not enabled, return early"); + bc.postMessage("close"); + } else { + offlineOnline(false); + } + } + } else if (event.data == "offline") { + ++offlineCount; + info("offline " + offlineCount); + if (offlineCount == 1) { + bc.postMessage("forward"); + } else if (offlineCount == 2) { + offlineOnline(true); + } else { + ok(false, "unexpected offline event"); + } + } else if (event.data == "online") { + ++onlineCount; + info("online " + onlineCount); + if (onlineCount == 1) { + bc.postMessage("back"); + } else if (onlineCount == 2) { + bc.postMessage("close"); + } else { + ok(false, "unexpected online event"); + } + } else if ("closed") { + ok(true, "Did pass the test"); + bc.close(); + SimpleTest.finish(); + } + }; + + function runTest() { + SpecialPowers.pushPrefEnv({"set": [["network.manage-offline-status", false]]}, function() { + window.open("file_online_offline_bfcache.html", "", "noopener"); + }); + } + + SimpleTest.waitForExplicitFinish(); + </script> +</head> +<body onload="runTest()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_open_javascript_noopener.html b/docshell/test/navigation/test_open_javascript_noopener.html new file mode 100644 index 0000000000..81a6b70d61 --- /dev/null +++ b/docshell/test/navigation/test_open_javascript_noopener.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script> + +add_task(async function test_open_javascript_noopener() { + const topic = "test-javascript-was-run"; + function jsuri(version) { + return `javascript:SpecialPowers.notifyObservers(null, "${topic}", "${version}");window.close()`; + } + + let seen = []; + function observer(_subject, _topic, data) { + info(`got notification ${data}`); + seen.push(data); + } + SpecialPowers.addObserver(observer, topic); + + isDeeply(seen, [], "seen no test notifications"); + window.open(jsuri("1")); + + // Bounce off the parent process to make sure the JS will have run. + await SpecialPowers.spawnChrome([], () => {}); + + isDeeply(seen, ["1"], "seen the opener notification"); + + window.open(jsuri("2"), "", "noopener"); + + // Bounce off the parent process to make sure the JS will have run. + await SpecialPowers.spawnChrome([], () => {}); + + isDeeply(seen, ["1"], "didn't get a notification from the noopener popup"); + + SpecialPowers.removeObserver(observer, topic); +}); + + </script> + </body> +</html> diff --git a/docshell/test/navigation/test_opener.html b/docshell/test/navigation/test_opener.html new file mode 100644 index 0000000000..ce966b897d --- /dev/null +++ b/docshell/test/navigation/test_opener.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 50px; } + </style> +<script> +if (navigator.platform.startsWith("Linux")) { + SimpleTest.expectAssertions(0, 1); +} + +window.onload = async function() { + navigateByLocation(window0); + navigateByOpen("window1"); + navigateByForm("window2"); + navigateByHyperlink("window3"); + + await waitForFinishedFrames(4); + await isNavigated(window0, "Should be able to navigate popup by setting location."); + await isNavigated(window1, "Should be able to navigate popup by calling window.open."); + await isNavigated(window2, "Should be able to navigate popup by submitting form."); + await isNavigated(window3, "Should be able to navigate popup by targeted hyperlink."); + + window0.close(); + window1.close(); + window2.close(); + window3.close(); + + await cleanupWindows(); + + SimpleTest.finish(); +}; + +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var window0 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/blank.html", "window0", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var window1 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/blank.html", "window1", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var window2 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/blank.html", "window2", "width=10,height=10"); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +var window3 = window.open("http://test1.example.org:80/tests/docshell/test/navigation/blank.html", "window3", "width=10,height=10"); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_performance_navigation.html b/docshell/test/navigation/test_performance_navigation.html new file mode 100644 index 0000000000..75abbdd767 --- /dev/null +++ b/docshell/test/navigation/test_performance_navigation.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=145971 +--> +<head> + <title>Test for Bug 145971</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=145971">Mozilla Bug 145971</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +var testWindow; +var bc = new BroadcastChannel("bug145971"); +bc.onmessage = function(msgEvent) { + var result = msgEvent.data.result; + if (result == undefined) { + info("Got unexpected message from BroadcastChannel"); + return; + } + ok(result, "Bug 145971: Navigation type does not equal 2 when restoring document from session history."); + SimpleTest.finish(); +}; + +function runTest() { + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + window.open("test_bug145971.html", "", "width=360,height=480,noopener"); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_popup-navigates-children.html b/docshell/test/navigation/test_popup-navigates-children.html new file mode 100644 index 0000000000..82d69e7982 --- /dev/null +++ b/docshell/test/navigation/test_popup-navigates-children.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 50px; } + </style> +<script> + +let window0 = null; +let window1 = null; +let window2 = null; +let window3 = null; + +function testChild0() { + if (!window.window0) + window0 = window.open("navigate.html#opener.frames[0],location", "window0", "width=10,height=10"); +} + +function testChild1() { + if (!window.window1) + window1 = window.open("navigate.html#child1,open", "window1", "width=10,height=10"); +} + +function testChild2() { + if (!window.window2) + window2 = window.open("navigate.html#child2,form", "window2", "width=10,height=10"); +} + +function testChild3() { + if (!window.window3) + window3 = window.open("navigate.html#child3,hyperlink", "window3", "width=10,height=10"); +} + +window.onload = async function() { + await waitForFinishedFrames(4); + await isNavigated(frames[0], "Should be able to navigate on-domain opener's children by setting location."); + await isNavigated(frames[1], "Should be able to navigate on-domain opener's children by calling window.open."); + await isNavigated(frames[2], "Should be able to navigate on-domain opener's children by submitting form."); + await isNavigated(frames[3], "Should be able to navigate on-domain opener's children by targeted hyperlink."); + + window0.close(); + window1.close(); + window2.close(); + window3.close(); + + await cleanupWindows(); + SimpleTest.finish(); +}; + +</script> +</head> +<body> +<div id="frames"> +<iframe onload="testChild0()" name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe onload="testChild1()" name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe onload="testChild2()" name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe onload="testChild3()" name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_rate_limit_location_change.html b/docshell/test/navigation/test_rate_limit_location_change.html new file mode 100644 index 0000000000..b1b51b92dd --- /dev/null +++ b/docshell/test/navigation/test_rate_limit_location_change.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1314912 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1314912</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1314912 */ + + const RATE_LIMIT_COUNT = 90; + const RATE_LIMIT_TIME_SPAN = 3; + + async function setup() { + await SpecialPowers.pushPrefEnv({set: [ + ["dom.navigation.locationChangeRateLimit.count", RATE_LIMIT_COUNT], + ["dom.navigation.locationChangeRateLimit.timespan", RATE_LIMIT_TIME_SPAN]]}); + } + + let inc = 0; + + const rateLimitedFunctions = (win) => ({ + "history.replaceState": () => win.history.replaceState(null, "test", `${win.location.href}#${inc++}`), + "history.pushState": () => win.history.pushState(null, "test", `${win.location.href}#${inc++}`), + "history.back": () => win.history.back(), + "history.forward": () => win.history.forward(), + "history.go": () => win.history.go(-1), + "location.href": () => win.location.href = win.location.href + "", + "location.hash": () => win.location.hash = inc++, + "location.host": () => win.location.host = win.location.host + "", + "location.hostname": () => win.location.hostname = win.location.hostname + "", + "location.pathname": () => win.location.pathname = win.location.pathname + "", + "location.port": () => win.location.port = win.location.port + "", + "location.protocol": () => win.location.protocol = win.location.protocol + "", + "location.search": () => win.location.search = win.location.search + "", + "location.assign": () => win.location.assign(`${win.location.href}#${inc++}`), + "location.replace": () => win.location.replace(`${win.location.href}#${inc++}`), + "location.reload": () => win.location.reload(), + }); + + async function test() { + await setup(); + + // Open new window and wait for it to load + let win = window.open("blank.html"); + await new Promise((resolve) => SimpleTest.waitForFocus(resolve, win)) + + // Execute the history and location functions + Object.entries(rateLimitedFunctions(win)).forEach(([name, fn]) => { + // Reset the rate limit for the next run. + info("Reset rate limit."); + SpecialPowers.wrap(win).browsingContext.resetLocationChangeRateLimit(); + + info(`Calling ${name} ${RATE_LIMIT_COUNT} times to reach the rate limit.`); + for(let i = 0; i< RATE_LIMIT_COUNT; i++) { + fn.call(this); + } + // Next calls should throw because we're above the rate limit + for(let i = 0; i < 5; i++) { + SimpleTest.doesThrow(() => fn.call(this), `Call #${RATE_LIMIT_COUNT + i + 1} to ${name} should throw.`); + } + }) + + // We didn't reset the rate limit after the last loop iteration above. + // Wait for the rate limit timer to expire. + SimpleTest.requestFlakyTimeout("Waiting to trigger rate limit reset."); + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Calls should be allowed again. + Object.entries(rateLimitedFunctions(win)).forEach(([name, fn]) => { + let didThrow = false; + try { + fn.call(this); + } catch(error) { + didThrow = true; + } + is(didThrow, false, `Call to ${name} must not throw.`) + }); + + // Cleanup + win.close(); + SpecialPowers.wrap(win).browsingContext.resetLocationChangeRateLimit(); + SimpleTest.finish(); + } + + </script> +</head> +<body onload="setTimeout(test, 0);"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1314912">Mozilla Bug 1314912</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_recursive_frames.html b/docshell/test/navigation/test_recursive_frames.html new file mode 100644 index 0000000000..3ccc09dd14 --- /dev/null +++ b/docshell/test/navigation/test_recursive_frames.html @@ -0,0 +1,167 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Recursive Loads</title> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1597427">Mozilla Bug 1597427</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + const TEST_CASES = [ + { // too many recursive iframes + frameId: "recursiveFrame", + expectedLocations: [ + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/frame_recursive.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/frame_recursive.html", + "about:blank", + ], + }, + { // too many recursive iframes + frameId: "twoRecursiveIframes", + expectedLocations: [ + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/frame_load_as_example_com.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/tests/docshell/test/navigation/frame_load_as_example_org.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/frame_load_as_example_com.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/tests/docshell/test/navigation/frame_load_as_example_org.html", + "about:blank", + ], + }, + { // too many recursive iframes + frameId: "threeRecursiveIframes", + expectedLocations: [ + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host1.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/frame_load_as_host2.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host3.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host1.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/frame_load_as_host2.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host3.html", + "about:blank", + ], + }, + { // too many nested iframes + frameId: "sixRecursiveIframes", + expectedLocations: [ + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/frame_1_out_of_6.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_2_out_of_6.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_3_out_of_6.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/frame_4_out_of_6.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://test2.mochi.test:8888/tests/docshell/test/navigation/frame_5_out_of_6.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/tests/docshell/test/navigation/frame_6_out_of_6.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/frame_1_out_of_6.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://test1.mochi.test:8888/tests/docshell/test/navigation/frame_2_out_of_6.html", + ], + }, + { // too many recursive objects + frameId: "recursiveObject", + expectedLocations: [ + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/object_recursive_load.html", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/object_recursive_load.html", + ], + }, + { // 3 nested srcdocs, should show all of them + frameId: "nestedSrcdoc", + expectedLocations: [ + "about:srcdoc", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/tests/docshell/test/navigation/file_nested_srcdoc.html", + "about:srcdoc", + "about:srcdoc", + ], + }, + ]; + + async function checkRecursiveLoad(level) { + let el = content.document.getElementById("static"); + let documentURI = await SpecialPowers.spawn( + el, + [], + () => this.content.document.documentURI + ); + if (documentURI == "about:blank") { + // If we had too many recursive frames, the most inner iframe's uri will be about:blank + return [documentURI]; + } + if (documentURI == "about:srcdoc" && level == 3) { + // Check that we have the correct most inner srcdoc iframe + let innerText = await SpecialPowers.spawn( + el, + [], + () => this.content.document.body.innerText + ); + is(innerText, "Third nested srcdoc", "correct most inner srcdoc iframe"); + } + let nestedIfrOrObjectURI = []; + try { + // Throws an error when we have too many nested frames/objects, because we + // claim to have no content window for the inner most frame/object. + nestedIfrOrObjectURI = await SpecialPowers.spawn( + el, + [level + 1], + checkRecursiveLoad + ); + } catch (err) { + info( + `Tried to spawn another task in the iframe/object, but got err: ${err}, must have had too many nested iframes/objects\n` + ); + } + return [documentURI, ...nestedIfrOrObjectURI]; + } + + add_task(async () => { + for (const testCase of TEST_CASES) { + let el = document.getElementById(testCase.frameId); + let loc = await SpecialPowers.spawn( + el, + [], + () => this.content.location.href + ); + let locations = await SpecialPowers.spawn(el, [1], checkRecursiveLoad); + isDeeply( + [loc, ...locations], + testCase.expectedLocations, + "iframes/object loaded in correct order" + ); + } + }); + + </script> +</pre> +<div> + <iframe style="height: 100vh; width:25%;" id="recursiveFrame" src="http://example.com/tests/docshell/test/navigation/frame_recursive.html"></iframe> + <iframe style="height: 100vh; width:25%;" id="twoRecursiveIframes" src="http://example.com/tests/docshell/test/navigation/frame_load_as_example_com.html"></iframe> + <iframe style="height: 100vh; width:25%;" id="threeRecursiveIframes" src="http://sub1.test1.mochi.test:8888/tests/docshell/test/navigation/frame_load_as_host1.html"></iframe> + <iframe style="height: 100vh; width:25%;" id="sixRecursiveIframes" src="http://example.com/tests/docshell/test/navigation/frame_1_out_of_6.html"></iframe> + <object width="400" height="300" id="recursiveObject" data="http://sub2.xn--lt-uia.mochi.test:8888/tests/docshell/test/navigation/object_recursive_load.html"></object> + <iframe id="nestedSrcdoc" srcdoc="Srcdoc that will embed an iframe <iframe id="static" src="http://example.com/tests/docshell/test/navigation/file_nested_srcdoc.html"></iframe>"></iframe> +</div> +</body> +</html> diff --git a/docshell/test/navigation/test_reload.html b/docshell/test/navigation/test_reload.html new file mode 100644 index 0000000000..7e75c7c035 --- /dev/null +++ b/docshell/test/navigation/test_reload.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Ensure a page which is otherwise bfcacheable doesn't crash on reload</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + let pageshowCount = 0; + let bc = new BroadcastChannel("test_reload"); + bc.onmessage = function(event) { + info("MessageEvent: " + event.data); + if (event.data == "pageshow") { + ++pageshowCount; + info("pageshow: " + pageshowCount); + if (pageshowCount < 3) { + info("Sending reload"); + bc.postMessage("reload"); + } else { + info("Sending close"); + bc.postMessage("close"); + } + } else if (event.data == "closed") { + info("closed"); + bc.close(); + ok(true, "Passed"); + SimpleTest.finish(); + } + } + + function test() { + window.open("file_reload.html", "", "noopener"); + } + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_reload_large_postdata.html b/docshell/test/navigation/test_reload_large_postdata.html new file mode 100644 index 0000000000..15fae33ac3 --- /dev/null +++ b/docshell/test/navigation/test_reload_large_postdata.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> + +<form id="form" action="file_reload_large_postdata.sjs" target="_blank" rel="opener" method="POST"> + <input id="input" name="payload" type="hidden" value=""/> +</form> + +<pre id="test"> +<script> +// This is derived from `kTooLargeStream` in `IPCStreamUtils.cpp`. +const kTooLargeStream = 1024 * 1024; + +function waitForPopup(expected) { + return new Promise(resolve => { + addEventListener("message", evt => { + info("got message!"); + is(evt.source.opener, window, "the event source's opener should be this window"); + is(evt.data, expected, "got the expected data from the popup"); + resolve(evt.source); + }, { once: true }); + }); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({"set": [["dom.confirm_repost.testing.always_accept", true]]}); + let form = document.getElementById("form"); + let input = document.getElementById("input"); + + // Create a very large value to include in the post payload. This should + // ensure that the value isn't sent directly over IPC, and is instead sent as + // an async inputstream. + let payloadSize = kTooLargeStream; + + let popupReady = waitForPopup(payloadSize); + input.value = "A".repeat(payloadSize); + form.submit(); + + let popup = await popupReady; + try { + let popupReady2 = waitForPopup(payloadSize); + info("reloading popup"); + popup.location.reload(); + let popup2 = await popupReady2; + is(popup, popup2); + } finally { + popup.close(); + } +}); + +// The .sjs server can time out processing the 1mb payload in debug builds. +SimpleTest.requestLongerTimeout(2); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_reload_nonbfcached_srcdoc.html b/docshell/test/navigation/test_reload_nonbfcached_srcdoc.html new file mode 100644 index 0000000000..2399a0ad7d --- /dev/null +++ b/docshell/test/navigation/test_reload_nonbfcached_srcdoc.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test srcdoc handling when reloading a page.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + // The old session history implementation asserts in + // https://searchfox.org/mozilla-central/rev/b822a27de3947d3f4898defac6164e52caf1451b/docshell/shistory/nsSHEntry.cpp#670-672 + SimpleTest.expectAssertions(0, 1); + SimpleTest.waitForExplicitFinish(); + + var win; + function test() { + window.onmessage = function(event) { + if (event.data == "pageload:") { + // Trigger a similar reload as what the reload button does. + SpecialPowers.wrap(win) + .docShell + .QueryInterface(SpecialPowers.Ci.nsIWebNavigation) + .sessionHistory + .reload(0); + } else if (event.data == "pageload:second") { + ok(true, "srcdoc iframe was updated."); + win.close(); + SimpleTest.finish(); + } + } + win = window.open("file_reload_nonbfcached_srcdoc.sjs"); + } + + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_reserved.html b/docshell/test/navigation/test_reserved.html new file mode 100644 index 0000000000..0242f3941b --- /dev/null +++ b/docshell/test/navigation/test_reserved.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 200px; } + </style> +<script> +if (navigator.platform.startsWith("Mac")) { + SimpleTest.expectAssertions(0, 2); +} + +async function testTop() { + let window0 = window.open("iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#top,location", "_blank", "width=10,height=10"); + + await waitForFinishedFrames(1); + isInaccessible(window0, "Should be able to navigate off-domain top by setting location."); + window0.close(); + await cleanupWindows(); + + let window1 = window.open("iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_top,open", "_blank", "width=10,height=10"); + + await waitForFinishedFrames(1); + isInaccessible(window1, "Should be able to navigate off-domain top by calling window.open."); + window1.close(); + await cleanupWindows(); + + let window2 = window.open("iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_top,form", "_blank", "width=10,height=10"); + + await waitForFinishedFrames(1); + isInaccessible(window2, "Should be able to navigate off-domain top by submitting form."); + window2.close(); + await cleanupWindows(); + + let window3 = window.open("iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_top,hyperlink", "_blank", "width=10,height=10"); + + await waitForFinishedFrames(1); + isInaccessible(window3, "Should be able to navigate off-domain top by targeted hyperlink."); + window3.close(); + await cleanupWindows(); + + await testParent(); +} + +async function testParent() { + document.getElementById("frames").innerHTML = '<iframe src="iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#parent,location"></iframe>'; + + await waitForFinishedFrames(1); + isAccessible(frames[0], "Should not be able to navigate off-domain parent by setting location."); + await cleanupWindows(); + + document.getElementById("frames").innerHTML = '<iframe src="iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_parent,open"></iframe>'; + + await waitForFinishedFrames(1); + isAccessible(frames[0], "Should not be able to navigate off-domain parent by calling window.open."); + await cleanupWindows(); + + document.getElementById("frames").innerHTML = '<iframe src="iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_parent,form"></iframe>'; + + await waitForFinishedFrames(1); + isAccessible(frames[0], "Should not be able to navigate off-domain parent by submitting form."); + await cleanupWindows(); + + document.getElementById("frames").innerHTML = '<iframe src="iframe.html#http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#_parent,hyperlink"></iframe>'; + + await waitForFinishedFrames(1); + isAccessible(frames[0], "Should not be able to navigate off-domain parent by targeted hyperlink."); + await cleanupWindows(); + + document.getElementById("frames").innerHTML = ""; + SimpleTest.finish(); +} + +window.onload = async function() { + await testTop(); +}; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a> +<div id="frames"> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_same_url.html b/docshell/test/navigation/test_same_url.html new file mode 100644 index 0000000000..820caa7005 --- /dev/null +++ b/docshell/test/navigation/test_same_url.html @@ -0,0 +1,56 @@ + +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="/tests/SimpleTest/SpecialPowers.js"></script> + <script type="application/javascript"> + // Since BFCache in parent requires no opener, use BroadcastChannel + // to communicate with file_same_url.html. + let bc = new BroadcastChannel("test_same_url"); + async function test() { + var promise; + let historyLength; + + promise = waitForLoad(); + window.open("file_same_url.html", "_blank", "noopener=yes"); + historyLength = await promise; + is(historyLength, 1, "before same page navigation"); + + promise = waitForLoad(); + bc.postMessage("linkClick"); + historyLength = await promise; + is(historyLength, 1, "after same page navigation"); + bc.postMessage("closeWin"); + + SimpleTest.finish(); + } + + async function waitForLoad() { + return new Promise(resolve => { + let listener = e => { + if (e.data.bodyOnLoad) { + bc.removeEventListener("message", listener); + setTimeout(() => resolve(e.data.bodyOnLoad), 0); + } + }; + bc.addEventListener("message", listener); + }); + } + </script> +</head> + +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1745730">Bug 1745730</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +<body onload="test()"> +</body> +</html> + diff --git a/docshell/test/navigation/test_scrollRestoration.html b/docshell/test/navigation/test_scrollRestoration.html new file mode 100644 index 0000000000..d31598f391 --- /dev/null +++ b/docshell/test/navigation/test_scrollRestoration.html @@ -0,0 +1,214 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 1155730</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155730">Mozilla Bug 1155730</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + + function assertCheck(data) { + if (data.assertIs) { + for (const args of data.assertIs) { + is(args[0], args[1], args[2]); + } + } + if (data.assertOk) { + for (const args of data.assertOk) { + ok(args[0], args[1]); + } + } + if (data.assertIsNot) { + for (const args of data.assertIsNot) { + isnot(args[0], args[1], args[2]); + } + } + } + + var bc1, currentCase = 0; + function test1() { + bc1 = new BroadcastChannel("bug1155730_part1"); + bc1.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "pageshow") { + currentCase++; + var persisted = msg.persisted; + is(persisted, false, "Shouldn't have persisted session history entry."); + bc1.postMessage({command: "test", currentCase}); + } else if (command == "asserts") { + is(msg.currentCase, currentCase, "correct case"); + info(`Checking asserts for case ${msg.currentCase}`); + assertCheck(msg); + if (currentCase == 3) { + // move on to the next test + bc1.close(); + test2(); + } + } + } + window.open("file_scrollRestoration_part1_nobfcache.html", "", "width=360,height=480,noopener"); + } + + var bc2, bc2navigate; + function test2() { + currentCase = 0; + bc2 = new BroadcastChannel("bug1155730_part2"); + bc2.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "pageshow") { + currentCase++; + var persisted = msg.persisted; + switch (currentCase) { + case 1: + is(persisted, false, "Shouldn't have persisted session history entry."); + break; + case 2: + is(persisted, true, "Should have persisted session history entry."); + } + bc2.postMessage({command: "test", currentCase}); + } else if (command == "asserts") { + is(msg.currentCase, currentCase, "correct case"); + info(`Checking asserts for case ${msg.currentCase}`); + assertCheck(msg); + if (currentCase == 3) { + // move on to the next test + bc2.close(); + test3(); + } + } else if (command == "nextCase") { + currentCase++; + } + } + + bc2navigate = new BroadcastChannel("navigate"); + bc2navigate.onmessage = (event) => { + if (event.data.command == "loaded") { + bc2navigate.postMessage({command: "back"}) + bc2navigate.close(); + } + } + window.open("file_scrollRestoration_part2_bfcache.html", "", "width=360,height=480,noopener"); + } + + var bc3, bc3navigate; + function test3() { + currentCase = 0; + bc3 = new BroadcastChannel("bug1155730_part3"); + bc3.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "pageshow") { + currentCase++; + if (currentCase == 3) { + var persisted = msg.persisted; + is(persisted, false, "Shouldn't have persisted session history entry."); + } + + bc3.postMessage({command: "test", currentCase}); + } else if (command == "asserts") { + is(msg.currentCase, currentCase, "correct case"); + info(`Checking asserts for case ${msg.currentCase}`); + assertCheck(msg); + } else if (command == "nextCase") { + currentCase++; + } else if (command == "finishing") { + bc3.close(); + test4(); + } + } + + bc3navigate = new BroadcastChannel("navigate"); + bc3navigate.onmessage = (event) => { + if (event.data.command == "loaded") { + is(event.data.scrollRestoration, 'auto', "correct scroll restoration"); + bc3navigate.postMessage({command: "back"}) + bc3navigate.close(); + } + } + window.open("file_scrollRestoration_part3_nobfcache.html", "", "width=360,height=480,noopener"); + } + + // test4 opens a new page which can enter bfcache. That page then loads + // another page which can't enter bfcache. That second page then scrolls + // down. History API is then used to navigate back and forward. When the + // second page loads again, it should scroll down automatically. + var bc4a, bc4b; + var scrollYCounter = 0; + function test4() { + currentCase = 0; + bc4a = new BroadcastChannel("bfcached"); + bc4a.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "pageshow") { + ++currentCase; + if (currentCase == 1) { + ok(!msg.persisted, "The first page should not be persisted initially."); + bc4a.postMessage("loadNext"); + } else if (currentCase == 3) { + ok(msg.persisted, "The first page should be persisted."); + bc4a.postMessage("forward"); + bc4a.close(); + } + } + } + + bc4b = new BroadcastChannel("notbfcached"); + bc4b.onmessage = (event) => { + var msg = event.data; + var command = msg.command; + if (command == "pageshow") { + ++currentCase; + if (currentCase == 2) { + ok(!msg.persisted, "The second page should not be persisted."); + bc4b.postMessage("getScrollY"); + bc4b.postMessage("scroll"); + bc4b.postMessage("getScrollY"); + bc4b.postMessage("back"); + } else if (currentCase == 4) { + ok(!msg.persisted, "The second page should not be persisted."); + bc4b.postMessage("getScrollY"); + } + } else if (msg == "closed") { + bc4b.close(); + SimpleTest.finish(); + } else if ("scrollY" in msg) { + ++scrollYCounter; + if (scrollYCounter == 1) { + is(msg.scrollY, 0, "The page should be initially scrolled to top."); + } else if (scrollYCounter == 2) { + isnot(msg.scrollY, 0, "The page should be then scrolled down."); + } else if (scrollYCounter == 3) { + isnot(msg.scrollY, 0, "The page should be scrolled down after being restored from the session history."); + bc4b.postMessage("close"); + } + } + } + window.open("file_scrollRestoration_bfcache_and_nobfcache.html", "", "width=360,height=480,noopener"); + } + + function runTest() { + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + test1(); + }); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_session_history_entry_cleanup.html b/docshell/test/navigation/test_session_history_entry_cleanup.html new file mode 100644 index 0000000000..a55de0d6c3 --- /dev/null +++ b/docshell/test/navigation/test_session_history_entry_cleanup.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 534178</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=534178">Mozilla Bug 534178</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_bug534178.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + testWindow.close(); + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_session_history_on_redirect.html b/docshell/test/navigation/test_session_history_on_redirect.html new file mode 100644 index 0000000000..a303f81536 --- /dev/null +++ b/docshell/test/navigation/test_session_history_on_redirect.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Session history on redirect</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + /* + * The test opens a new window and loads a page there. Then another document + * is loaded to the window. The initial load of that second page doesn't do + * a redirect. Now another non-redirecting page is loaded. Then + * history.go(-2) and history.forward() are called. The second time + * the second page is loaded, it does a redirect. history.back() and + * history.forward() are called again. The page which did the redirect + * shouldn't be accessed, but the page which it redirected to. + * Finally history.forward() is called again and the third page should be + * loaded and history.length should have the same value as it had when the + * third page was loaded the first time. + */ + + SimpleTest.waitForExplicitFinish(); + var win; + var finalHistoryLength = 0; + + function run() { + win = window.open("file_session_history_on_redirect.html"); + } + + var pageshowCounter = 0; + async function pageshow() { + // Need to trigger new loads asynchronously after page load, otherwise + // new loads are treated as replace loads. + await new Promise((r) => setTimeout(r)); + ++pageshowCounter; + info("Page load: " + win.location.href); + switch (pageshowCounter) { + case 1: + ok(win.location.href.includes("file_session_history_on_redirect.html")); + win.location.href = "redirect_handlers.sjs"; + break; + case 2: + ok(win.location.href.includes("redirect_handlers.sjs")); + // Put the initial page also as the last entry in the session history. + win.location.href = "file_session_history_on_redirect.html"; + break; + case 3: + ok(win.location.href.includes("file_session_history_on_redirect.html")); + finalHistoryLength = win.history.length; + win.history.go(-2); + break; + case 4: + ok(win.location.href.includes("file_session_history_on_redirect.html")); + win.history.forward(); + break; + case 5: + ok(win.location.href.includes("file_session_history_on_redirect_2.html")); + win.history.back(); + break; + case 6: + ok(win.location.href.includes("file_session_history_on_redirect.html")); + win.history.forward(); + break; + case 7: + ok(win.location.href.includes("file_session_history_on_redirect_2.html")); + is(win.history.length, finalHistoryLength, "Shouldn't have changed the history length."); + win.history.forward(); + break; + case 8: + ok(win.location.href.includes("file_session_history_on_redirect.html")); + is(win.history.length, finalHistoryLength, "Shouldn't have changed the history length."); + win.onpagehide = null; + finishTest(); + break; + default: + ok(false, "unexpected pageshow"); + } + } + + function finishTest() { + win.close() + SimpleTest.finish(); + } + + </script> +</head> +<body onload="run()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_sessionhistory.html b/docshell/test/navigation/test_sessionhistory.html new file mode 100644 index 0000000000..2254ec876b --- /dev/null +++ b/docshell/test/navigation/test_sessionhistory.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 462076</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="nextTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=462076">Mozilla Bug 462076</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + var testFiles = + [ "file_bug462076_1.html", // Dynamic frames before onload + "file_bug462076_2.html", // Dynamic frames when handling onload + "file_bug462076_3.html", // Dynamic frames after onload + ]; + var testCount = 0; // Used by the test files. + + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function nextTest_() { + if (testFiles.length) { + testCount = 0; + let nextFile = testFiles.shift(); + info("Running " + nextFile); + testWindow = window.open(nextFile, "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } else { + SimpleTest.finish(); + } + } + + function nextTest() { + setTimeout(nextTest_, 0); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_sessionhistory_document_write.html b/docshell/test/navigation/test_sessionhistory_document_write.html new file mode 100644 index 0000000000..2a48a8154e --- /dev/null +++ b/docshell/test/navigation/test_sessionhistory_document_write.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Session history + document.write</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_document_write_1.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + testWindow.close(); + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_sessionhistory_iframe_removal.html b/docshell/test/navigation/test_sessionhistory_iframe_removal.html new file mode 100644 index 0000000000..242e3baade --- /dev/null +++ b/docshell/test/navigation/test_sessionhistory_iframe_removal.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Session history + document.write</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_sessionhistory_iframe_removal.html", "", "width=360,height=480"); + } + + function finishTest() { + testWindow.close(); + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_shiftReload_and_pushState.html b/docshell/test/navigation/test_shiftReload_and_pushState.html new file mode 100644 index 0000000000..7525e2e21f --- /dev/null +++ b/docshell/test/navigation/test_shiftReload_and_pushState.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for Bug 1003100</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1003100">Mozilla Bug 1003100</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_shiftReload_and_pushState.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + testWindow.close(); + SimpleTest.finish(); + } + + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_ship_beforeunload_fired.html b/docshell/test/navigation/test_ship_beforeunload_fired.html new file mode 100644 index 0000000000..e43711676b --- /dev/null +++ b/docshell/test/navigation/test_ship_beforeunload_fired.html @@ -0,0 +1,63 @@ +<html> + <head> + <title> + Test that ensures beforeunload is fired when session-history-in-parent is enabled + </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + </head> + <script> + SimpleTest.waitForExplicitFinish(); + + /* + * This test ensures beforeunload is fired on the current page + * when it is entering BFCache and the next page is coming out + * from BFCache + * + * (1) The controller page opens a new window, and page A is loaded there. + * (2) Page A then navigates to page B, and a beforeunload event + * listener is registered on page B. + * (3) Page B then navigates back to page A, and the beforeunload handler + * should send a message to the controller page. + * (4) Page A then navigates back to page B to check if page B has + * been successfully added to BFCache. + */ + + var bc = new BroadcastChannel("ship_beforeunload"); + var pageshowCount = 0; + var beforeUnloadFired = false; + bc.onmessage = function(event) { + if (event.data.type == "pageshow") { + ++pageshowCount; + if (pageshowCount == 1) { + bc.postMessage({action: "navigate_to_page_b"}); + } else if (pageshowCount == 2) { + ok(!event.data.persisted, "?pageb shouldn't in BFCache because it's the first navigation"); + bc.postMessage({action: "register_beforeunload", loadNextPageFromSessionHistory: true}); + } else if (pageshowCount == 3) { + ok(event.data.persisted, "navigated back to page A that was in BFCacache from page B"); + ok(beforeUnloadFired, "beforeunload has fired on page B"); + bc.postMessage({action: "back_to_page_b", forwardNavigateToPageB: true}); + } else if (pageshowCount == 4) { + ok(event.data.persisted, "page B has beforeunload fired and also entered BFCache"); + bc.postMessage({action: "close"}); + SimpleTest.finish(); + } + } else if (event.data == "beforeunload_fired") { + beforeUnloadFired = true; + } + } + + function runTest() { + SpecialPowers.pushPrefEnv({"set": [ + ["fission.bfcacheInParent", true], + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", true] + ]}, + function() { + window.open("file_ship_beforeunload_fired.html", "", "noopener"); + } + ); + } + </script> + <body onload="runTest()"></body> +</html> diff --git a/docshell/test/navigation/test_ship_beforeunload_fired_2.html b/docshell/test/navigation/test_ship_beforeunload_fired_2.html new file mode 100644 index 0000000000..93669502a5 --- /dev/null +++ b/docshell/test/navigation/test_ship_beforeunload_fired_2.html @@ -0,0 +1,65 @@ +<html> + <head> + <title> + Test that ensures beforeunload is fired when session-history-in-parent is enabled + </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + </head> + <script> + SimpleTest.waitForExplicitFinish(); + + /* + * This test ensures beforeunload is fired on the current page + * when it is entering BFCache, and the next page is not coming from + * session history and also not coming out from BFCache. + * + * (1) The controller page opens a new window, and page A is loaded there. + * (2) Page A then navigates to page B, and a beforeunload event + * listener is registered on page B. + * (3) Page B then navigates to page C, and the beforeunload handler + * should send a message to the controller page. + * (4) Page C then navigates back to page B to check if page B has + * been successfully added to BFCache. + */ + + var bc = new BroadcastChannel("ship_beforeunload"); + var pageshowCount = 0; + + var beforeUnloadFired = false; + bc.onmessage = function(event) { + if (event.data.type == "pageshow") { + ++pageshowCount; + if (pageshowCount == 1) { + bc.postMessage({action: "navigate_to_page_b"}); + } else if (pageshowCount == 2) { + ok(!event.data.persisted, "?page B shouldn't in BFCache because it's the first navigation"); + bc.postMessage({action: "register_beforeunload", + loadNextPageFromSessionHistory: false}); + } else if (pageshowCount == 3) { + ok(!event.data.persisted, "navigated to page C that was a new page"); + ok(beforeUnloadFired, "beforeUnload should be fired on page B"); + bc.postMessage({action: "back_to_page_b", forwardNavigateToPageB: false}); + } else if (pageshowCount == 4) { + ok(event.data.persisted, "page B has been successfully added to BFCache"); + bc.postMessage({action: "close"}); + SimpleTest.finish(); + } + } else if (event.data == "beforeunload_fired") { + beforeUnloadFired = true; + } + } + + function runTest() { + SpecialPowers.pushPrefEnv({"set": [ + ["fission.bfcacheInParent", true], + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", true] + ]}, + function() { + window.open("file_ship_beforeunload_fired.html", "", "noopener"); + } + ); + } + </script> + <body onload="runTest()"></body> +</html> diff --git a/docshell/test/navigation/test_ship_beforeunload_fired_3.html b/docshell/test/navigation/test_ship_beforeunload_fired_3.html new file mode 100644 index 0000000000..8951f269c5 --- /dev/null +++ b/docshell/test/navigation/test_ship_beforeunload_fired_3.html @@ -0,0 +1,65 @@ +<html> + <head> + <title> + Test that ensures beforeunload is fired when session-history-in-parent is enabled + </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + </head> + <script> + SimpleTest.waitForExplicitFinish(); + + /* + * This test ensures beforeunload is fired on the current page + * when it is entering BFCache, and the next page is not coming out + * from BFCache, but coming from session history. + * + * (1) The controller page opens a new window, and page A is loaded there. + * (2) Page A then navigates to page B, and a beforeunload event + * listener is registered on page B. + * (3) Page B then navigates back to page A, and the beforeunload handler + * should send a message to the controller page. + * (4) Page A then navigates back to page B to check if page B has + * been successfully added to BFCache. + */ + + var bc = new BroadcastChannel("ship_beforeunload"); + var pageshowCount = 0; + + var beforeUnloadFired = false; + bc.onmessage = function(event) { + if (event.data.type == "pageshow") { + ++pageshowCount; + if (pageshowCount == 1) { + bc.postMessage({action: "navigate_to_page_b", blockBFCache: true}); + } else if (pageshowCount == 2) { + ok(!event.data.persisted, "?page B shouldn't in BFCache because it's the first navigation"); + bc.postMessage({action: "register_beforeunload", + loadNextPageFromSessionHistory: true}); + } else if (pageshowCount == 3) { + ok(!event.data.persisted, "navigated back to page A that was session history but not in BFCache"); + ok(beforeUnloadFired, "beforeUnload should be fired on page B"); + bc.postMessage({action: "back_to_page_b", forwardNavigateToPageB: true}); + } else if (pageshowCount == 4) { + ok(event.data.persisted, "page B has been successfully added to BFCache"); + bc.postMessage({action: "close"}); + SimpleTest.finish(); + } + } else if (event.data == "beforeunload_fired") { + beforeUnloadFired = true; + } + } + + function runTest() { + SpecialPowers.pushPrefEnv({"set": [ + ["fission.bfcacheInParent", true], + ["docshell.shistory.bfcache.ship_allow_beforeunload_listeners", true] + ]}, + function() { + window.open("file_ship_beforeunload_fired.html", "", "noopener"); + } + ); + } + </script> + <body onload="runTest()"></body> +</html> diff --git a/docshell/test/navigation/test_sibling-matching-parent.html b/docshell/test/navigation/test_sibling-matching-parent.html new file mode 100644 index 0000000000..3c1bc768db --- /dev/null +++ b/docshell/test/navigation/test_sibling-matching-parent.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 50px; } + </style> +<script> +window.onload = async function() { + document.getElementById("active").innerHTML = + '<iframe src="navigate.html#parent.frames[0],location"></iframe>' + + '<iframe src="navigate.html#child1,open"></iframe>' + + '<iframe src="navigate.html#child2,form"></iframe>' + + '<iframe src="navigate.html#child3,hyperlink"></iframe>'; + + await waitForFinishedFrames(4); + + await isNavigated(frames[0], "Should be able to navigate sibling with on-domain parent by setting location."); + await isNavigated(frames[1], "Should be able to navigate sibling with on-domain parent by calling window.open."); + await isNavigated(frames[2], "Should be able to navigate sibling with on-domain parent by submitting form."); + await isNavigated(frames[3], "Should be able to navigate sibling with on-domain parent by targeted hyperlink."); + + await cleanupWindows(); + SimpleTest.finish(); +}; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a> +<div id="frames"> +<iframe name="child0" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe name="child1" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe name="child2" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +<iframe name="child3" src="http://test1.example.org:80/tests/docshell/test/navigation/blank.html"></iframe> +</div> +<div id="active"></div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_sibling-off-domain.html b/docshell/test/navigation/test_sibling-off-domain.html new file mode 100644 index 0000000000..cd70d1ae91 --- /dev/null +++ b/docshell/test/navigation/test_sibling-off-domain.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> + <style type="text/css"> + iframe { width: 90%; height: 50px; } + </style> +<script> +window.onload = async function() { + document.getElementById("active").innerHTML = + '<iframe src="http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#parent.frames[0],location"></iframe>' + + '<iframe src="http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#child1,open"></iframe>' + + '<iframe src="http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#child2,form"></iframe>' + + '<iframe src="http://test1.example.org:80/tests/docshell/test/navigation/navigate.html#child3,hyperlink"></iframe>'; + + await waitForFinishedFrames(4); + + isBlank(frames[0], "Should not be able to navigate off-domain sibling by setting location."); + isBlank(frames[1], "Should not be able to navigate off-domain sibling by calling window.open."); + isBlank(frames[2], "Should not be able to navigate off-domain sibling by submitting form."); + isBlank(frames[3], "Should not be able to navigate off-domain sibling by targeted hyperlink."); + + await cleanupWindows(); + SimpleTest.finish(); +}; +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408052">Mozilla Bug 408052</a> +<div id="frames"> +<iframe name="child0" src="blank.html"></iframe> +<iframe name="child1" src="blank.html"></iframe> +<iframe name="child2" src="blank.html"></iframe> +<iframe name="child3" src="blank.html"></iframe> +</div> +<div id="active"></div> +<pre id="test"> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_state_size.html b/docshell/test/navigation/test_state_size.html new file mode 100644 index 0000000000..f089a460ec --- /dev/null +++ b/docshell/test/navigation/test_state_size.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test the max size of the data parameter of push/replaceState</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + function test() { + let tooLarge = SpecialPowers.getIntPref("browser.history.maxStateObjectSize"); + let allowed = Math.floor(tooLarge / 2); + + history.pushState(new Array(allowed).join("a"), ""); + ok(true, "Adding a state should succeed."); + + try { + history.pushState(new Array(tooLarge).join("a"), ""); + ok(false, "Adding a too large state object should fail."); + } catch(ex) { + ok(true, "Adding a too large state object should fail."); + } + SimpleTest.finish(); + } + </script> +</head> +<body onload="test()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/docshell/test/navigation/test_static_and_dynamic.html b/docshell/test/navigation/test_static_and_dynamic.html new file mode 100644 index 0000000000..ff72a8188c --- /dev/null +++ b/docshell/test/navigation/test_static_and_dynamic.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id= +--> +<head> + <title>Test for static and dynamic frames and forward-back </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + var testCount = 0; // Used by the test files. + + SimpleTest.waitForExplicitFinish(); + + var testWindow; + function runTest() { + testWindow = window.open("file_static_and_dynamic_1.html", "", "width=360,height=480"); + testWindow.onunload = function() { }; // to prevent bfcache + } + + function finishTest() { + testWindow.close(); + SimpleTest.finish(); + } + </script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_triggeringprincipal_frame_nav.html b/docshell/test/navigation/test_triggeringprincipal_frame_nav.html new file mode 100644 index 0000000000..d7046e9236 --- /dev/null +++ b/docshell/test/navigation/test_triggeringprincipal_frame_nav.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1181370 - Test triggeringPrincipal for iframe navigations</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe1"></iframe> +<iframe style="width:100%;" id="testframe2"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * + * +------------------------------------+ + * | +----------+ +--------------+ | + * | | Frame 1 | | Frame 2 | | + * | +----------+ | | | + * | | +----------+ | | + * | | | Subframe | | | + * | | +----------+ | | + * | +--------------+ | + * +------------------------------------+ + * + * Frame1: test1.mochi.test + * Frame2: test2.mochi.test + * Subframe: test2.mochi.test + * + * (*) Frame1 and Subframe set their document.domain to mochi.test + * (*) Frame1 navigates the Subframe + * (*) TriggeringPrincipal for the Subframe navigation should be + * ==> test1.mochi.test + * (*) LoadingPrincipal for the Subframe navigation should be + * ==> test2.mochi.test + */ + +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const BASEDOMAIN1 = "http://test1.mochi.test:8888/"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const BASEDOMAIN2 = "http://test2.mochi.test:8888/"; +const PATH = "tests/docshell/test/navigation/"; +const BASEURL1 = BASEDOMAIN1 + PATH; +const BASEURL2 = BASEDOMAIN2 + PATH; +const TRIGGERINGPRINCIPALURI = BASEURL1 + "file_triggeringprincipal_frame_1.html"; +const LOADINGPRINCIPALURI = BASEURL2 + "file_triggeringprincipal_frame_2.html"; + +SimpleTest.waitForExplicitFinish(); + +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) { + is(event.data.triggeringPrincipalURI, TRIGGERINGPRINCIPALURI, + "TriggeringPrincipal should be the navigating iframe (Frame 1)"); + is(event.data.loadingPrincipalURI, LOADINGPRINCIPALURI, + "LoadingPrincipal should be the enclosing iframe (Frame 2)"); + is(event.data.referrerURI, BASEDOMAIN1, + "The path of Referrer should be trimmed (Frame 1)"); + + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +var frame1 = document.getElementById("testframe1"); +frame1.src = BASEURL1 + "file_triggeringprincipal_frame_1.html"; + +var frame2 = document.getElementById("testframe2"); +frame2.src = BASEURL2 + "file_triggeringprincipal_frame_2.html"; + +</script> +</body> +</html> diff --git a/docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html b/docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html new file mode 100644 index 0000000000..4483efb13e --- /dev/null +++ b/docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1639195 - Test triggeringPrincipal for iframe same-origin navigations</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe" src="http://example.com/"></iframe> + +<script type="text/javascript"> +/* We load an third-party iframe which then gets navigated by the iframe's + * parent by calling iframe.setAttribute("src", same-origin url) later in the + * test. We then verify the TriggeringPrincipal and LoadingPrincipal of the + * navigated iframe. + * + * +------------------------------------------+ + * | | + * | +------------------+ | + * | | testframe | | + * | +------------------+ | + * | | + * | iframe.setAttribute("src", | + * | same-origin url) | + * | | + * +------------------------------------------+ + */ + +var testframe = document.getElementById("testframe"); + +window.addEventListener("message", receiveMessage); + +const TRIGGERING_PRINCIPAL_URI = + "http://mochi.test:8888/tests/docshell/test/navigation/test_triggeringprincipal_frame_same_origin_nav.html"; + +const LOADING_PRINCIPAL_URI = TRIGGERING_PRINCIPAL_URI; + +function receiveMessage(event) { + is(event.data.triggeringPrincipalURI.split("?")[0], TRIGGERING_PRINCIPAL_URI, + "TriggeringPrincipal should be the parent iframe"); + is(event.data.loadingPrincipalURI.split("?")[0], TRIGGERING_PRINCIPAL_URI, + "LoadingPrincipal should be the parent iframe"); + + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +function performNavigation() { + testframe.removeEventListener("load", performNavigation); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + testframe.setAttribute("src", "http://example.com/tests/docshell/test/navigation/file_triggeringprincipal_subframe_same_origin_nav.html"); +} + +// start the test +SimpleTest.waitForExplicitFinish(); + +testframe.addEventListener("load", performNavigation); +</script> + +</body> +</html> diff --git a/docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html b/docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html new file mode 100644 index 0000000000..115c5f4462 --- /dev/null +++ b/docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> +</head> +<body> + +<iframe name="framea" id="framea" src="file_triggeringprincipal_iframe_iframe_window_open_frame_a.html"></iframe> +<iframe name="frameb" id="frameb"></iframe> + +<script type="text/javascript"> + +/* We load an iframe (Frame A) which then gets navigated by another iframe (Frame B) + * by calling window.open("http://", "Frame A") later in the test. We then verify the + * TriggeringPrincipal and LoadingPrincipal of the navigated iframe (Frame A). + * + * +---------------------------------------+ + * | Parent | + * | | + * | +----------------------------+ | + * | | Frame A | | + * | | | | + * | | | | + * | +----------------------------+ | + * | | + * | +----------------------------+ | + * | | Frame B | | + * | | | | + * | | win.open("http://", "A") | | + * | +----------------------------+ | + * | | + * +---------------------------------------+ + * + * Sequence of the test: + * [1] load Frame A + * [2] load Frame B which navigates A + * [3] load navigated Frame A and check triggeringPrincipal and loadingPrincipal + */ + +const TRIGGERING_PRINCIPAL_URI = + "http://mochi.test:8888/tests/docshell/test/navigation/file_triggeringprincipal_iframe_iframe_window_open_frame_b.html"; + +const LOADING_PRINCIPAL_URI = + "http://mochi.test:8888/tests/docshell/test/navigation/test_triggeringprincipal_iframe_iframe_window_open.html"; + +var frameA = document.getElementById("framea"); + +function checkResults() { + frameA.removeEventListener("load", checkResults); + + var channel = SpecialPowers.wrap(frameA.contentWindow).docShell.currentDocumentChannel; + var triggeringPrincipal = channel.loadInfo.triggeringPrincipal.asciiSpec; + var loadingPrincipal = channel.loadInfo.loadingPrincipal.asciiSpec; + + is(triggeringPrincipal, TRIGGERING_PRINCIPAL_URI, + "TriggeringPrincipal for targeted window.open() should be the iframe triggering the load"); + + is(frameA.contentDocument.referrer, TRIGGERING_PRINCIPAL_URI, + "Referrer for targeted window.open() should be the principal of the iframe triggering the load"); + + is(loadingPrincipal.split("?")[0], LOADING_PRINCIPAL_URI, + "LoadingPrincipal for targeted window.open() should be the containing document"); + + SimpleTest.finish(); +} + +function performNavigation() { + frameA.removeEventListener("load", performNavigation); + frameA.addEventListener("load", checkResults); + + // load Frame B which then navigates Frame A + var frameB = document.getElementById("frameb"); + frameB.src = "file_triggeringprincipal_iframe_iframe_window_open_frame_b.html"; +} + +// start the test +SimpleTest.waitForExplicitFinish(); + +frameA.addEventListener("load", performNavigation); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html b/docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html new file mode 100644 index 0000000000..1611ebf479 --- /dev/null +++ b/docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> +</head> +<body> + +<iframe name="testframe" id="testframe" src="file_triggeringprincipal_iframe_iframe_window_open_base.html"></iframe> + +<script type="text/javascript"> + +/* We load an iframe which then gets navigated by the iframe's parent by calling + * window.open("http://", iframe) later in the test. We then verify the + * TriggeringPrincipal and LoadingPrincipal of the navigated iframe. + * + * +------------------------------------------+ + * | | + * | +------------------+ | + * | | testframe | | + * | +------------------+ | + * | | + * | window.open("http://", "testframe"); | + * | | + * +------------------------------------------+ + */ + +const TRIGGERING_PRINCIPAL_URI = + "http://mochi.test:8888/tests/docshell/test/navigation/test_triggeringprincipal_parent_iframe_window_open.html"; + +const LOADING_PRINCIPAL_URI = TRIGGERING_PRINCIPAL_URI; + +var testframe = document.getElementById("testframe"); + +function checkResults() { + testframe.removeEventListener("load", checkResults); + + var channel = SpecialPowers.wrap(testframe.contentWindow).docShell.currentDocumentChannel; + var triggeringPrincipal = channel.loadInfo.triggeringPrincipal.asciiSpec.split("?")[0]; + var loadingPrincipal = channel.loadInfo.loadingPrincipal.asciiSpec.split("?")[0]; + + is(triggeringPrincipal, TRIGGERING_PRINCIPAL_URI, + "TriggeringPrincipal for targeted window.open() should be the principal of the document"); + + is(testframe.contentDocument.referrer.split("?")[0], TRIGGERING_PRINCIPAL_URI, + "Referrer for targeted window.open() should be the principal of the document"); + + is(loadingPrincipal, LOADING_PRINCIPAL_URI, + "LoadingPrincipal for targeted window.open() should be the <iframe>.ownerDocument"); + + SimpleTest.finish(); +} + +function performNavigation() { + testframe.removeEventListener("load", performNavigation); + testframe.addEventListener("load", checkResults); + window.open("file_triggeringprincipal_parent_iframe_window_open_nav.html", "testframe"); +} + +// start the test +SimpleTest.waitForExplicitFinish(); + +testframe.addEventListener("load", performNavigation); + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/navigation/test_triggeringprincipal_window_open.html b/docshell/test/navigation/test_triggeringprincipal_window_open.html new file mode 100644 index 0000000000..439a125f97 --- /dev/null +++ b/docshell/test/navigation/test_triggeringprincipal_window_open.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="NavigationUtils.js"></script> +</head> +<body> + +<script type="text/javascript"> + +/* We call window.open() using different URIs and make sure the triggeringPrincipal + * loadingPrincipal are correct. + * Test1: window.open(http:) + * Test2: window.open(javascript:) + */ + +const TRIGGERING_PRINCIPAL_URI = + "http://mochi.test:8888/tests/docshell/test/navigation/test_triggeringprincipal_window_open.html"; + +SimpleTest.waitForExplicitFinish(); + +const NUM_TESTS = 2; +var test_counter = 0; + +function checkFinish() { + test_counter++; + if (test_counter === NUM_TESTS) { + SimpleTest.finish(); + } +} + +// ---------------------------------------------------------------------------- +// Test 1: window.open(http:) +var httpWin = window.open("file_triggeringprincipal_window_open.html", "_blank", "width=10,height=10"); +httpWin.onload = function() { + var httpChannel = SpecialPowers.wrap(httpWin).docShell.currentDocumentChannel; + var httpTriggeringPrincipal = httpChannel.loadInfo.triggeringPrincipal.asciiSpec; + var httpLoadingPrincipal = httpChannel.loadInfo.loadingPrincipal; + + is(httpTriggeringPrincipal.split("?")[0], TRIGGERING_PRINCIPAL_URI, + "TriggeringPrincipal for window.open(http:) should be the principal of the document"); + + is(httpWin.document.referrer.split("?")[0], TRIGGERING_PRINCIPAL_URI, + "Referrer for window.open(http:) should be the principal of the document"); + + is(httpLoadingPrincipal, null, + "LoadingPrincipal for window.open(http:) should be null"); + + httpWin.close(); + checkFinish(); +}; + +// ---------------------------------------------------------------------------- +// Test 2: window.open(javascript:) +var jsWin = window.open("javascript:'<html><body>js</body></html>';", "_blank", "width=10,height=10"); +jsWin.onload = function() { + var jsChannel = SpecialPowers.wrap(jsWin).docShell.currentDocumentChannel; + var jsTriggeringPrincipal = jsChannel.loadInfo.triggeringPrincipal.asciiSpec; + var jsLoadingPrincipal = jsChannel.loadInfo.loadingPrincipal; + + is(jsTriggeringPrincipal.split("?")[0], TRIGGERING_PRINCIPAL_URI, + "TriggeringPrincipal for window.open(javascript:) should be the principal of the document"); + + is(jsWin.document.referrer, "", + "Referrer for window.open(javascript:) should be empty"); + + is(jsLoadingPrincipal, null, + "LoadingPrincipal for window.open(javascript:) should be null"); + + jsWin.close(); + checkFinish(); +}; + +</script> +</pre> +</body> +</html> diff --git a/docshell/test/unit/AllowJavascriptChild.sys.mjs b/docshell/test/unit/AllowJavascriptChild.sys.mjs new file mode 100644 index 0000000000..a7c3fa6172 --- /dev/null +++ b/docshell/test/unit/AllowJavascriptChild.sys.mjs @@ -0,0 +1,41 @@ +export class AllowJavascriptChild extends JSWindowActorChild { + async receiveMessage(msg) { + switch (msg.name) { + case "CheckScriptsAllowed": + return this.checkScriptsAllowed(); + case "CheckFiredLoadEvent": + return this.contentWindow.wrappedJSObject.gFiredOnload; + case "CreateIframe": + return this.createIframe(msg.data.url); + } + return null; + } + + handleEvent(event) { + if (event.type === "load") { + this.sendAsyncMessage("LoadFired"); + } + } + + checkScriptsAllowed() { + let win = this.contentWindow; + + win.wrappedJSObject.gFiredOnclick = false; + win.document.body.click(); + return win.wrappedJSObject.gFiredOnclick; + } + + async createIframe(url) { + let doc = this.contentWindow.document; + + let iframe = doc.createElement("iframe"); + iframe.src = url; + doc.body.appendChild(iframe); + + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + + return iframe.browsingContext; + } +} diff --git a/docshell/test/unit/AllowJavascriptParent.sys.mjs b/docshell/test/unit/AllowJavascriptParent.sys.mjs new file mode 100644 index 0000000000..5631fcdb09 --- /dev/null +++ b/docshell/test/unit/AllowJavascriptParent.sys.mjs @@ -0,0 +1,28 @@ +let loadPromises = new WeakMap(); + +export class AllowJavascriptParent extends JSWindowActorParent { + async receiveMessage(msg) { + switch (msg.name) { + case "LoadFired": + let bc = this.browsingContext; + let deferred = loadPromises.get(bc); + if (deferred) { + loadPromises.delete(bc); + deferred.resolve(this); + } + break; + } + } + + static promiseLoad(bc) { + let deferred = loadPromises.get(bc); + if (!deferred) { + deferred = {}; + deferred.promise = new Promise(resolve => { + deferred.resolve = resolve; + }); + loadPromises.set(bc, deferred); + } + return deferred.promise; + } +} diff --git a/docshell/test/unit/data/engine.xml b/docshell/test/unit/data/engine.xml new file mode 100644 index 0000000000..3a2bd85c1b --- /dev/null +++ b/docshell/test/unit/data/engine.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>test_urifixup_search_engine</ShortName> +<Description>test_urifixup_search_engine</Description> +<InputEncoding>UTF-8</InputEncoding> +<Url type="text/html" method="GET" template="https://www.example.org/"> + <Param name="search" value="{searchTerms}"/> +</Url> +<SearchForm>https://www.example.org/</SearchForm> +</SearchPlugin> diff --git a/docshell/test/unit/data/enginePost.xml b/docshell/test/unit/data/enginePost.xml new file mode 100644 index 0000000000..14775b6f0a --- /dev/null +++ b/docshell/test/unit/data/enginePost.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>test_urifixup_search_engine_post</ShortName> +<Description>test_urifixup_search_engine_post</Description> +<InputEncoding>UTF-8</InputEncoding> +<Url type="text/html" method="POST" template="https://www.example.org/"> + <Param name="q" value="{searchTerms}"/> +</Url> +<SearchForm>https://www.example.org/</SearchForm> +</SearchPlugin> diff --git a/docshell/test/unit/data/enginePrivate.xml b/docshell/test/unit/data/enginePrivate.xml new file mode 100644 index 0000000000..7d87de98fa --- /dev/null +++ b/docshell/test/unit/data/enginePrivate.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>test_urifixup_search_engine_private</ShortName> +<Description>test_urifixup_search_engine_private</Description> +<InputEncoding>UTF-8</InputEncoding> +<Url type="text/html" method="GET" template="https://www.example.org/"> + <Param name="private" value="{searchTerms}"/> +</Url> +<SearchForm>https://www.example.org/</SearchForm> +</SearchPlugin> diff --git a/docshell/test/unit/head_docshell.js b/docshell/test/unit/head_docshell.js new file mode 100644 index 0000000000..5b0369089e --- /dev/null +++ b/docshell/test/unit/head_docshell.js @@ -0,0 +1,106 @@ +/* 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/. */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", + NetUtil: "resource://gre/modules/NetUtil.jsm", +}); + +var profileDir = do_get_profile(); + +const kSearchEngineID = "test_urifixup_search_engine"; +const kSearchEngineURL = "https://www.example.org/?search={searchTerms}"; +const kPrivateSearchEngineID = "test_urifixup_search_engine_private"; +const kPrivateSearchEngineURL = + "https://www.example.org/?private={searchTerms}"; +const kPostSearchEngineID = "test_urifixup_search_engine_post"; +const kPostSearchEngineURL = "https://www.example.org/"; +const kPostSearchEngineData = "q={searchTerms}"; + +const SEARCH_CONFIG = [ + { + appliesTo: [ + { + included: { + everywhere: true, + }, + }, + ], + default: "yes", + webExtension: { + id: "fixup_search@search.mozilla.org", + }, + }, +]; + +async function setupSearchService() { + SearchTestUtils.init(this); + + AddonTestUtils.init(this); + AddonTestUtils.overrideCertDB(); + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "42" + ); + + await SearchTestUtils.useTestEngines(".", null, SEARCH_CONFIG); + await AddonTestUtils.promiseStartupManager(); + await Services.search.init(); +} + +/** + * After useHttpServer() is called, this string contains the URL of the "data" + * directory, including the final slash. + */ +var gDataUrl; + +/** + * Initializes the HTTP server and ensures that it is terminated when tests end. + * + * @param {string} dir + * The test sub-directory to use for the engines. + * @returns {HttpServer} + * The HttpServer object in case further customization is needed. + */ +function useHttpServer(dir = "data") { + let httpServer = new HttpServer(); + httpServer.start(-1); + httpServer.registerDirectory("/", do_get_cwd()); + gDataUrl = `http://localhost:${httpServer.identity.primaryPort}/${dir}/`; + registerCleanupFunction(async function cleanup_httpServer() { + await new Promise(resolve => { + httpServer.stop(resolve); + }); + }); + return httpServer; +} + +async function addTestEngines() { + useHttpServer(); + // This is a hack, ideally we should be setting up a configuration with + // built-in engines, but the `chrome_settings_overrides` section that + // WebExtensions need is only defined for browser/ + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}/engine.xml`, + }); + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}/enginePrivate.xml`, + }); + await SearchTestUtils.promiseNewSearchEngine({ + url: `${gDataUrl}/enginePost.xml`, + }); +} diff --git a/docshell/test/unit/test_URIFixup.js b/docshell/test/unit/test_URIFixup.js new file mode 100644 index 0000000000..7967933b56 --- /dev/null +++ b/docshell/test/unit/test_URIFixup.js @@ -0,0 +1,123 @@ +var pref = "browser.fixup.typo.scheme"; + +var data = [ + { + // ttp -> http. + wrong: "ttp://www.example.com/", + fixed: "http://www.example.com/", + }, + { + // htp -> http. + wrong: "htp://www.example.com/", + fixed: "http://www.example.com/", + }, + { + // ttps -> https. + wrong: "ttps://www.example.com/", + fixed: "https://www.example.com/", + }, + { + // tps -> https. + wrong: "tps://www.example.com/", + fixed: "https://www.example.com/", + }, + { + // ps -> https. + wrong: "ps://www.example.com/", + fixed: "https://www.example.com/", + }, + { + // htps -> https. + wrong: "htps://www.example.com/", + fixed: "https://www.example.com/", + }, + { + // ile -> file. + wrong: "ile:///this/is/a/test.html", + fixed: "file:///this/is/a/test.html", + }, + { + // le -> file. + wrong: "le:///this/is/a/test.html", + fixed: "file:///this/is/a/test.html", + }, + { + // Replace ';' with ':'. + wrong: "http;//www.example.com/", + fixed: "http://www.example.com/", + noPrefValue: "http://http;//www.example.com/", + }, + { + // Missing ':'. + wrong: "https//www.example.com/", + fixed: "https://www.example.com/", + noPrefValue: "http://https//www.example.com/", + }, + { + // Missing ':' for file scheme. + wrong: "file///this/is/a/test.html", + fixed: "file:///this/is/a/test.html", + noPrefValue: "http://file///this/is/a/test.html", + }, + { + // Valid should not be changed. + wrong: "https://example.com/this/is/a/test.html", + fixed: "https://example.com/this/is/a/test.html", + }, + { + // Unmatched should not be changed. + wrong: "whatever://this/is/a/test.html", + fixed: "whatever://this/is/a/test.html", + }, +]; + +var len = data.length; + +add_task(async function setup() { + await setupSearchService(); + // Now we've initialised the search service, we force remove the engines + // it has, so they don't interfere with this test. + // Search engine integration is tested in test_URIFixup_search.js. + Services.search.wrappedJSObject._engines.clear(); +}); + +// Make sure we fix what needs fixing when there is no pref set. +add_task(function test_unset_pref_fixes_typos() { + Services.prefs.clearUserPref(pref); + for (let i = 0; i < len; ++i) { + let item = data[i]; + let { preferredURI } = Services.uriFixup.getFixupURIInfo( + item.wrong, + Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS + ); + Assert.equal(preferredURI.spec, item.fixed); + } +}); + +// Make sure we don't do anything when the pref is explicitly +// set to false. +add_task(function test_false_pref_keeps_typos() { + Services.prefs.setBoolPref(pref, false); + for (let i = 0; i < len; ++i) { + let item = data[i]; + let { preferredURI } = Services.uriFixup.getFixupURIInfo( + item.wrong, + Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS + ); + Assert.equal(preferredURI.spec, item.noPrefValue || item.wrong); + } +}); + +// Finally, make sure we still fix what needs fixing if the pref is +// explicitly set to true. +add_task(function test_true_pref_fixes_typos() { + Services.prefs.setBoolPref(pref, true); + for (let i = 0; i < len; ++i) { + let item = data[i]; + let { preferredURI } = Services.uriFixup.getFixupURIInfo( + item.wrong, + Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS + ); + Assert.equal(preferredURI.spec, item.fixed); + } +}); diff --git a/docshell/test/unit/test_URIFixup_check_host.js b/docshell/test/unit/test_URIFixup_check_host.js new file mode 100644 index 0000000000..132d74ca9b --- /dev/null +++ b/docshell/test/unit/test_URIFixup_check_host.js @@ -0,0 +1,183 @@ +/* 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/. */ + +"use strict"; + +const lazy = {}; + +// Ensure DNS lookups don't hit the server +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gDNSOverride", + "@mozilla.org/network/native-dns-override;1", + "nsINativeDNSResolverOverride" +); + +add_task(async function setup() { + Services.prefs.setStringPref("browser.fixup.alternate.prefix", "www."); + Services.prefs.setStringPref("browser.fixup.alternate.suffix", ".com"); + Services.prefs.setStringPref("browser.fixup.alternate.protocol", "https"); + Services.prefs.setBoolPref( + "browser.urlbar.dnsResolveFullyQualifiedNames", + true + ); + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.fixup.alternate.prefix"); + Services.prefs.clearUserPref("browser.fixup.alternate.suffix"); + Services.prefs.clearUserPref("browser.fixup.alternate.protocol"); + Services.prefs.clearUserPref( + "browser.urlbar.dnsResolveFullyQualifiedNames" + ); + }); +}); + +// TODO: Export Listener from test_dns_override instead of duping +class Listener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + + async firstAddress() { + let all = await this.addresses(); + if (all.length) { + return all[0]; + } + return undefined; + } + + async addresses() { + let [, inRecord] = await this.promise; + let addresses = []; + if (!inRecord) { + return addresses; // returns [] + } + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + while (inRecord.hasMore()) { + addresses.push(inRecord.getNextAddrAsString()); + } + return addresses; + } + + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +const FAKE_IP = "::1"; +const FAKE_INTRANET_IP = "::2"; +const ORIGIN_ATTRIBUTE = {}; + +add_task(async function test_uri_with_force_fixup() { + let listener = new Listener(); + let { fixedURI } = Services.uriFixup.forceHttpFixup("http://www.example.com"); + + lazy.gDNSOverride.addIPOverride(fixedURI.displayHost, FAKE_IP); + + Services.uriFixup.checkHost(fixedURI, listener, ORIGIN_ATTRIBUTE); + Assert.equal( + await listener.firstAddress(), + FAKE_IP, + "Should've received fake IP" + ); + + lazy.gDNSOverride.clearHostOverride(fixedURI.displayHost); + Services.dns.clearCache(false); +}); + +add_task(async function test_uri_with_get_fixup() { + let listener = new Listener(); + let uri = Services.io.newURI("http://www.example.com"); + + lazy.gDNSOverride.addIPOverride(uri.displayHost, FAKE_IP); + + Services.uriFixup.checkHost(uri, listener, ORIGIN_ATTRIBUTE); + Assert.equal( + await listener.firstAddress(), + FAKE_IP, + "Should've received fake IP" + ); + + lazy.gDNSOverride.clearHostOverride(uri.displayHost); + Services.dns.clearCache(false); +}); + +add_task(async function test_intranet_like_uri() { + let listener = new Listener(); + let uri = Services.io.newURI("http://someintranet"); + + lazy.gDNSOverride.addIPOverride(uri.displayHost, FAKE_IP); + // Hosts without periods should end with a period to make them FQN + lazy.gDNSOverride.addIPOverride(uri.displayHost + ".", FAKE_INTRANET_IP); + + Services.uriFixup.checkHost(uri, listener, ORIGIN_ATTRIBUTE); + Assert.deepEqual( + await listener.addresses(), + FAKE_INTRANET_IP, + "Should've received fake intranet IP" + ); + + lazy.gDNSOverride.clearHostOverride(uri.displayHost); + lazy.gDNSOverride.clearHostOverride(uri.displayHost + "."); + Services.dns.clearCache(false); +}); + +add_task(async function test_intranet_like_uri_without_fixup() { + let listener = new Listener(); + let uri = Services.io.newURI("http://someintranet"); + Services.prefs.setBoolPref( + "browser.urlbar.dnsResolveFullyQualifiedNames", + false + ); + + lazy.gDNSOverride.addIPOverride(uri.displayHost, FAKE_IP); + // Hosts without periods should end with a period to make them FQN + lazy.gDNSOverride.addIPOverride(uri.displayHost + ".", FAKE_INTRANET_IP); + + Services.uriFixup.checkHost(uri, listener, ORIGIN_ATTRIBUTE); + Assert.deepEqual( + await listener.addresses(), + FAKE_IP, + "Should've received non-fixed up IP" + ); + + lazy.gDNSOverride.clearHostOverride(uri.displayHost); + lazy.gDNSOverride.clearHostOverride(uri.displayHost + "."); + Services.dns.clearCache(false); +}); + +add_task(async function test_ip_address() { + let listener = new Listener(); + let uri = Services.io.newURI("http://192.168.0.1"); + + // Testing IP address being passed into the method + // requires observing if the asyncResolve method gets called + let didResolve = false; + let topic = "uri-fixup-check-dns"; + let observer = { + observe(aSubject, aTopicInner, aData) { + if (aTopicInner == topic) { + didResolve = true; + } + }, + }; + Services.obs.addObserver(observer, topic); + lazy.gDNSOverride.addIPOverride(uri.displayHost, FAKE_IP); + + Services.uriFixup.checkHost(uri, listener, ORIGIN_ATTRIBUTE); + Assert.equal( + didResolve, + false, + "Passing an IP address should not conduct a host lookup" + ); + + lazy.gDNSOverride.clearHostOverride(uri.displayHost); + Services.dns.clearCache(false); + Services.obs.removeObserver(observer, topic); +}); diff --git a/docshell/test/unit/test_URIFixup_external_protocol_fallback.js b/docshell/test/unit/test_URIFixup_external_protocol_fallback.js new file mode 100644 index 0000000000..0846070b7a --- /dev/null +++ b/docshell/test/unit/test_URIFixup_external_protocol_fallback.js @@ -0,0 +1,106 @@ +/* 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/. */ + +"use strict"; + +// Test whether fallback mechanism is working if don't trust nsIExternalProtocolService. + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +add_task(async function setup() { + info( + "Prepare mock nsIExternalProtocolService whose externalProtocolHandlerExists returns always true" + ); + const externalProtocolService = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + const mockId = MockRegistrar.register( + "@mozilla.org/uriloader/external-protocol-service;1", + { + getProtocolHandlerInfo: scheme => + externalProtocolService.getProtocolHandlerInfo(scheme), + externalProtocolHandlerExists: () => true, + QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]), + } + ); + const mockExternalProtocolService = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + Assert.ok( + mockExternalProtocolService.externalProtocolHandlerExists("__invalid__"), + "Mock service is working" + ); + + info("Register new dummy protocol"); + const dummyProtocolHandlerInfo = + externalProtocolService.getProtocolHandlerInfo("dummy"); + const handlerService = Cc[ + "@mozilla.org/uriloader/handler-service;1" + ].getService(Ci.nsIHandlerService); + handlerService.store(dummyProtocolHandlerInfo); + + info("Prepare test search engine"); + await setupSearchService(); + await addTestEngines(); + await Services.search.setDefault( + Services.search.getEngineByName(kSearchEngineID), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + registerCleanupFunction(() => { + handlerService.remove(dummyProtocolHandlerInfo); + MockRegistrar.unregister(mockId); + }); +}); + +add_task(function basic() { + const testData = [ + { + input: "mailto:test@example.com", + expected: + isSupportedInHandlerService("mailto") || + // Thunderbird IS a mailto handler, it doesn't have handlers. + AppConstants.MOZ_APP_NAME == "thunderbird" + ? "mailto:test@example.com" + : "http://mailto:test@example.com/", + }, + { + input: "keyword:search", + expected: "https://www.example.org/?search=keyword%3Asearch", + }, + { + input: "dummy:protocol", + expected: "dummy:protocol", + }, + ]; + + for (const { input, expected } of testData) { + assertFixup(input, expected); + } +}); + +function assertFixup(input, expected) { + const { preferredURI } = Services.uriFixup.getFixupURIInfo( + input, + Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS + ); + Assert.equal(preferredURI.spec, expected); +} + +function isSupportedInHandlerService(scheme) { + const externalProtocolService = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + const handlerService = Cc[ + "@mozilla.org/uriloader/handler-service;1" + ].getService(Ci.nsIHandlerService); + return handlerService.exists( + externalProtocolService.getProtocolHandlerInfo(scheme) + ); +} diff --git a/docshell/test/unit/test_URIFixup_forced.js b/docshell/test/unit/test_URIFixup_forced.js new file mode 100644 index 0000000000..1d4dfd94a7 --- /dev/null +++ b/docshell/test/unit/test_URIFixup_forced.js @@ -0,0 +1,159 @@ +/* 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/. */ + +"use strict"; + +var data = [ + { + // singleword. + wrong: "http://example/", + fixed: "https://www.example.com/", + }, + { + // upgrade protocol. + wrong: "http://www.example.com/", + fixed: "https://www.example.com/", + noAlternateURI: true, + }, + { + // no difference. + wrong: "https://www.example.com/", + fixed: "https://www.example.com/", + noProtocolFixup: true, + noAlternateURI: true, + }, + { + // don't add prefix when there's more than one dot. + wrong: "http://www.example.abc.def/", + fixed: "https://www.example.abc.def/", + noAlternateURI: true, + }, + { + // http -> https. + wrong: "http://www.example/", + fixed: "https://www.example/", + noAlternateURI: true, + }, + { + // domain.com -> https://www.domain.com. + wrong: "http://example.com/", + fixed: "https://www.example.com/", + }, + { + // example/example... -> https://www.example.com/example/ + wrong: "http://example/example/", + fixed: "https://www.example.com/example/", + }, + { + // example/example/s#q -> www.example.com/example/s#q. + wrong: "http://example/example/s#q", + fixed: "https://www.example.com/example/s#q", + }, + { + wrong: "http://モジラ.org", + fixed: "https://www.xn--yck6dwa.org/", + }, + { + wrong: "http://モジラ", + fixed: "https://www.xn--yck6dwa.com/", + }, + { + wrong: "http://xn--yck6dwa", + fixed: "https://www.xn--yck6dwa.com/", + }, + { + wrong: "https://モジラ.org", + fixed: "https://www.xn--yck6dwa.org/", + noProtocolFixup: true, + }, + { + wrong: "https://モジラ", + fixed: "https://www.xn--yck6dwa.com/", + noProtocolFixup: true, + }, + { + wrong: "https://xn--yck6dwa", + fixed: "https://www.xn--yck6dwa.com/", + noProtocolFixup: true, + }, + { + // Host is https -> fixup typos of protocol + wrong: "htp://https://mozilla.org", + fixed: "http://https//mozilla.org", + noAlternateURI: true, + }, + { + // Host is http -> fixup typos of protocol + wrong: "ttp://http://mozilla.org", + fixed: "http://http//mozilla.org", + noAlternateURI: true, + }, + { + // Host is localhost -> fixup typos of protocol + wrong: "htps://localhost://mozilla.org", + fixed: "https://localhost//mozilla.org", + noAlternateURI: true, + }, + { + // view-source is not http/https -> error + wrong: "view-source:http://example/example/example/example", + reject: true, + comment: "Scheme should be either http or https", + }, + { + // file is not http/https -> error + wrong: "file://http://example/example/example/example", + reject: true, + comment: "Scheme should be either http or https", + }, + { + // Protocol is missing -> error + wrong: "example.com", + reject: true, + comment: "Scheme should be either http or https", + }, + { + // Empty input -> error + wrong: "", + reject: true, + comment: "Should pass a non-null uri", + }, +]; + +add_task(async function setup() { + Services.prefs.setStringPref("browser.fixup.alternate.prefix", "www."); + Services.prefs.setStringPref("browser.fixup.alternate.suffix", ".com"); + Services.prefs.setStringPref("browser.fixup.alternate.protocol", "https"); + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.fixup.alternate.prefix"); + Services.prefs.clearUserPref("browser.fixup.alternate.suffix"); + Services.prefs.clearUserPref("browser.fixup.alternate.protocol"); + }); +}); + +add_task(function test_default_https_pref() { + for (let item of data) { + if (item.reject) { + Assert.throws( + () => Services.uriFixup.forceHttpFixup(item.wrong), + /NS_ERROR_FAILURE/, + item.comment + ); + } else { + let { fixupChangedProtocol, fixupCreatedAlternateURI, fixedURI } = + Services.uriFixup.forceHttpFixup(item.wrong); + Assert.equal(fixedURI.spec, item.fixed, "Specs should be the same"); + Assert.equal( + fixupChangedProtocol, + !item.noProtocolFixup, + `fixupChangedProtocol should be ${!item.noAlternateURI}` + ); + Assert.equal( + fixupCreatedAlternateURI, + !item.noAlternateURI, + `fixupCreatedAlternateURI should be ${!item.limitedFixup}` + ); + } + } +}); diff --git a/docshell/test/unit/test_URIFixup_info.js b/docshell/test/unit/test_URIFixup_info.js new file mode 100644 index 0000000000..89514cb00c --- /dev/null +++ b/docshell/test/unit/test_URIFixup_info.js @@ -0,0 +1,1075 @@ +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const kForceDNSLookup = "browser.fixup.dns_first_for_single_words"; +const kFixupEnabled = "browser.fixup.alternate.enabled"; + +// TODO(bug 1522134), this test should also use +// combinations of the following flags. +var flagInputs = [ + Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP, + Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP | + Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT, + Services.uriFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI, + Services.uriFixup.FIXUP_FLAG_FORCE_ALTERNATE_URI, + Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS, + // This should not really generate a search, but it does, see Bug 1588118. + Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | + Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT, +]; + +/* + The following properties are supported for these test cases: + { + input: "", // Input string, required + fixedURI: "", // Expected fixedURI + alternateURI: "", // Expected alternateURI + keywordLookup: false, // Whether a keyword lookup is expected + protocolChange: false, // Whether a protocol change is expected + inWhitelist: false, // Whether the input host is in the whitelist + affectedByDNSForSingleWordHosts: false, // Whether the input host could be a host, but is normally assumed to be a keyword query + } +*/ +var testcases = [ + { + input: "about:home", + fixedURI: "about:home", + }, + { + input: "http://www.mozilla.org", + fixedURI: "http://www.mozilla.org/", + }, + { + input: "http://127.0.0.1/", + fixedURI: "http://127.0.0.1/", + }, + { + input: "file:///foo/bar", + fixedURI: "file:///foo/bar", + }, + { + input: "://www.mozilla.org", + fixedURI: "http://www.mozilla.org/", + protocolChange: true, + }, + { + input: "www.mozilla.org", + fixedURI: "http://www.mozilla.org/", + protocolChange: true, + }, + { + input: "http://mozilla/", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + }, + { + input: "http://test./", + fixedURI: "http://test./", + alternateURI: "https://www.test./", + }, + { + input: "127.0.0.1", + fixedURI: "http://127.0.0.1/", + protocolChange: true, + }, + { + input: "1.2.3.4/", + fixedURI: "http://1.2.3.4/", + protocolChange: true, + }, + { + input: "1.2.3.4/foo", + fixedURI: "http://1.2.3.4/foo", + protocolChange: true, + }, + { + input: "1.2.3.4:8000", + fixedURI: "http://1.2.3.4:8000/", + protocolChange: true, + }, + { + input: "1.2.3.4:8000/", + fixedURI: "http://1.2.3.4:8000/", + protocolChange: true, + }, + { + input: "1.2.3.4:8000/foo", + fixedURI: "http://1.2.3.4:8000/foo", + protocolChange: true, + }, + { + input: "192.168.10.110", + fixedURI: "http://192.168.10.110/", + protocolChange: true, + }, + { + input: "192.168.10.110/123", + fixedURI: "http://192.168.10.110/123", + protocolChange: true, + }, + { + input: "192.168.10.110/123foo", + fixedURI: "http://192.168.10.110/123foo", + protocolChange: true, + }, + { + input: "192.168.10.110:1234/123", + fixedURI: "http://192.168.10.110:1234/123", + protocolChange: true, + }, + { + input: "192.168.10.110:1234/123foo", + fixedURI: "http://192.168.10.110:1234/123foo", + protocolChange: true, + }, + { + input: "1.2.3", + fixedURI: "http://1.2.0.3/", + protocolChange: true, + }, + { + input: "1.2.3/", + fixedURI: "http://1.2.0.3/", + protocolChange: true, + }, + { + input: "1.2.3/foo", + fixedURI: "http://1.2.0.3/foo", + protocolChange: true, + }, + { + input: "1.2.3/123", + fixedURI: "http://1.2.0.3/123", + protocolChange: true, + }, + { + input: "1.2.3:8000", + fixedURI: "http://1.2.0.3:8000/", + protocolChange: true, + }, + { + input: "1.2.3:8000/", + fixedURI: "http://1.2.0.3:8000/", + protocolChange: true, + }, + { + input: "1.2.3:8000/foo", + fixedURI: "http://1.2.0.3:8000/foo", + protocolChange: true, + }, + { + input: "1.2.3:8000/123", + fixedURI: "http://1.2.0.3:8000/123", + protocolChange: true, + }, + { + input: "http://1.2.3", + fixedURI: "http://1.2.0.3/", + }, + { + input: "http://1.2.3/", + fixedURI: "http://1.2.0.3/", + }, + { + input: "http://1.2.3/foo", + fixedURI: "http://1.2.0.3/foo", + }, + { + input: "[::1]", + fixedURI: "http://[::1]/", + protocolChange: true, + }, + { + input: "[::1]/", + fixedURI: "http://[::1]/", + protocolChange: true, + }, + { + input: "[::1]:8000", + fixedURI: "http://[::1]:8000/", + protocolChange: true, + }, + { + input: "[::1]:8000/", + fixedURI: "http://[::1]:8000/", + protocolChange: true, + }, + { + input: "[[::1]]/", + keywordLookup: true, + }, + { + input: "[fe80:cd00:0:cde:1257:0:211e:729c]", + fixedURI: "http://[fe80:cd00:0:cde:1257:0:211e:729c]/", + protocolChange: true, + }, + { + input: "[64:ff9b::8.8.8.8]", + fixedURI: "http://[64:ff9b::808:808]/", + protocolChange: true, + }, + { + input: "[64:ff9b::8.8.8.8]/~moz", + fixedURI: "http://[64:ff9b::808:808]/~moz", + protocolChange: true, + }, + { + input: "[::1][::1]", + keywordLookup: true, + }, + { + input: "[::1][100", + keywordLookup: true, + }, + { + input: "[::1]]", + keywordLookup: true, + }, + { + input: "1234", + fixedURI: "http://0.0.4.210/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "whitelisted/foo.txt", + fixedURI: "http://whitelisted/foo.txt", + alternateURI: "https://www.whitelisted.com/foo.txt", + protocolChange: true, + }, + { + input: "mozilla", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "test.", + fixedURI: "http://test./", + alternateURI: "https://www.test./", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: ".test", + fixedURI: "http://.test/", + alternateURI: "https://www.test/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "mozilla is amazing", + keywordLookup: true, + }, + { + input: "search ?mozilla", + keywordLookup: true, + }, + { + input: "mozilla .com", + keywordLookup: true, + }, + { + input: "what if firefox?", + keywordLookup: true, + }, + { + input: "london's map", + keywordLookup: true, + }, + { + input: "mozilla ", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: " mozilla ", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "mozilla \\", + keywordLookup: true, + }, + { + input: "mozilla \\ foo.txt", + keywordLookup: true, + }, + { + input: "mozilla \\\r foo.txt", + keywordLookup: true, + }, + { + input: "mozilla\n", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "mozilla \r\n", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "moz\r\nfirefox\nos\r", + fixedURI: "http://mozfirefoxos/", + alternateURI: "https://www.mozfirefoxos.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "moz\r\n firefox\n", + keywordLookup: true, + }, + { + input: "", + keywordLookup: true, + }, + { + input: "[]", + keywordLookup: true, + }, + { + input: "http://whitelisted/", + fixedURI: "http://whitelisted/", + alternateURI: "https://www.whitelisted.com/", + inWhitelist: true, + }, + { + input: "whitelisted", + fixedURI: "http://whitelisted/", + alternateURI: "https://www.whitelisted.com/", + protocolChange: true, + inWhitelist: true, + }, + { + input: "whitelisted.", + fixedURI: "http://whitelisted./", + alternateURI: "https://www.whitelisted./", + protocolChange: true, + inWhitelist: true, + }, + { + input: "mochi.test", + fixedURI: "http://mochi.test/", + alternateURI: "https://www.mochi.test/", + protocolChange: true, + inWhitelist: true, + }, + // local.domain is a whitelisted suffix... + { + input: "some.local.domain", + fixedURI: "http://some.local.domain/", + protocolChange: true, + inWhitelist: true, + }, + // ...but .domain is not. + { + input: "some.domain", + fixedURI: "http://some.domain/", + alternateURI: "https://www.some.domain/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "café.com", + fixedURI: "http://xn--caf-dma.com/", + alternateURI: "https://www.xn--caf-dma.com/", + protocolChange: true, + }, + { + input: "mozilla.nonexistent", + fixedURI: "http://mozilla.nonexistent/", + alternateURI: "https://www.mozilla.nonexistent/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "mochi.ocm", + fixedURI: "http://mochi.com/", + alternateURI: "https://www.mochi.com/", + protocolChange: true, + }, + { + input: "47.6182,-122.830", + fixedURI: "http://47.6182,-122.830/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "-47.6182,-23.51", + fixedURI: "http://-47.6182,-23.51/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "-22.14,23.51-", + fixedURI: "http://-22.14,23.51-/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "32.7", + fixedURI: "http://32.0.0.7/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "5+2", + fixedURI: "http://5+2/", + alternateURI: "https://www.5+2.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "5/2", + fixedURI: "http://0.0.0.5/2", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "moz ?.::%27", + keywordLookup: true, + }, + { + input: "mozilla.com/?q=search", + fixedURI: "http://mozilla.com/?q=search", + alternateURI: "https://www.mozilla.com/?q=search", + protocolChange: true, + }, + { + input: "mozilla.com?q=search", + fixedURI: "http://mozilla.com/?q=search", + alternateURI: "https://www.mozilla.com/?q=search", + protocolChange: true, + }, + { + input: "mozilla.com ?q=search", + keywordLookup: true, + }, + { + input: "mozilla.com.?q=search", + fixedURI: "http://mozilla.com./?q=search", + protocolChange: true, + }, + { + input: "mozilla.com'?q=search", + fixedURI: "http://mozilla.com/?q=search", + alternateURI: "https://www.mozilla.com/?q=search", + protocolChange: true, + }, + { + input: "mozilla.com':search", + keywordLookup: true, + }, + { + input: "[mozilla]", + keywordLookup: true, + }, + { + input: "':?", + fixedURI: "http://'/?", + alternateURI: "https://www.'.com/?", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "whitelisted?.com", + fixedURI: "http://whitelisted/?.com", + alternateURI: "https://www.whitelisted.com/?.com", + protocolChange: true, + }, + { + input: "?'.com", + keywordLookup: true, + }, + { + input: ".com", + keywordLookup: true, + affectedByDNSForSingleWordHosts: true, + fixedURI: "http://.com/", + alternateURI: "https://www.com/", + protocolChange: true, + }, + { + input: "' ?.com", + keywordLookup: true, + }, + { + input: "?mozilla", + keywordLookup: true, + }, + { + input: "??mozilla", + keywordLookup: true, + }, + { + input: "mozilla/", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + protocolChange: true, + }, + { + input: "mozilla", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + protocolChange: true, + keywordLookup: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "mozilla5/2", + fixedURI: "http://mozilla5/2", + alternateURI: "https://www.mozilla5.com/2", + protocolChange: true, + keywordLookup: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "mozilla/foo", + fixedURI: "http://mozilla/foo", + alternateURI: "https://www.mozilla.com/foo", + protocolChange: true, + keywordLookup: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "mozilla\\", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "localhost", + fixedURI: "http://localhost/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "http", + fixedURI: "http://http/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "https", + fixedURI: "http://https/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "localhost:8080", + fixedURI: "http://localhost:8080/", + protocolChange: true, + }, + { + input: "plonk:8080", + fixedURI: "http://plonk:8080/", + protocolChange: true, + }, + { + input: "plonk:8080?test", + fixedURI: "http://plonk:8080/?test", + protocolChange: true, + }, + { + input: "plonk:8080#test", + fixedURI: "http://plonk:8080/#test", + protocolChange: true, + }, + { + input: "plonk/ #", + fixedURI: "http://plonk/%20#", + alternateURI: "https://www.plonk.com/%20#", + protocolChange: true, + keywordLookup: false, + }, + { + input: "blah.com.", + fixedURI: "http://blah.com./", + protocolChange: true, + }, + { + input: + "\u10E0\u10D4\u10D2\u10D8\u10E1\u10E2\u10E0\u10D0\u10EA\u10D8\u10D0.\u10D2\u10D4", + fixedURI: "http://xn--lodaehvb5cdik4g.xn--node/", + alternateURI: "https://www.xn--lodaehvb5cdik4g.xn--node/", + protocolChange: true, + }, + { + input: " \t mozilla.org/\t \t ", + fixedURI: "http://mozilla.org/", + alternateURI: "https://www.mozilla.org/", + protocolChange: true, + }, + { + input: " moz\ti\tlla.org ", + keywordLookup: true, + }, + { + input: "mozilla/", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + protocolChange: true, + }, + { + input: "mozilla/ test /", + fixedURI: "http://mozilla/%20test%20/", + alternateURI: "https://www.mozilla.com/%20test%20/", + protocolChange: true, + }, + { + input: "mozilla /test/", + keywordLookup: true, + }, + { + input: "pserver:8080", + fixedURI: "http://pserver:8080/", + protocolChange: true, + }, + { + input: "http;mozilla", + fixedURI: "http://http;mozilla/", + alternateURI: "https://www.http;mozilla.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }, + { + input: "http//mozilla.org", + fixedURI: "http://mozilla.org/", + shouldRunTest: flags => + flags & Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS, + }, + { + input: "http//mozilla.org", + fixedURI: "http://http//mozilla.org", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + shouldRunTest: flags => + !(flags & Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS), + }, + { + input: "www.mozilla", + fixedURI: "http://www.mozilla/", + protocolChange: true, + }, + { + input: "https://sub.www..mozilla...org/", + fixedURI: "https://sub.www.mozilla.org/", + }, + { + input: "sub.www..mozilla...org/", + fixedURI: "http://sub.www.mozilla.org/", + protocolChange: true, + }, + { + input: "sub.www..mozilla...org", + fixedURI: "http://sub.www.mozilla.org/", + protocolChange: true, + }, + { + input: "www...mozilla", + fixedURI: "http://www.mozilla/", + keywordLookup: true, + protocolChange: true, + shouldRunTest: flags => + !gSingleWordDNSLookup && + flags & Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP, + }, + { + input: "www...mozilla", + fixedURI: "http://www.mozilla/", + protocolChange: true, + shouldRunTest: flags => + gSingleWordDNSLookup || + !(flags & Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP), + }, + { + input: "mozilla...org", + fixedURI: "http://mozilla.org/", + protocolChange: true, + }, + { + input: "モジラ...org", + fixedURI: "http://xn--yck6dwa.org/", + protocolChange: true, + }, + { + input: "user@localhost", + fixedURI: "http://user@localhost/", + protocolChange: true, + shouldRunTest: () => gSingleWordDNSLookup, + }, + { + input: "user@localhost", + fixedURI: "http://user@localhost/", + keywordLookup: true, + protocolChange: true, + shouldRunTest: () => !gSingleWordDNSLookup, + }, + { + input: "user@192.168.0.1", + fixedURI: "http://user@192.168.0.1/", + protocolChange: true, + }, + { + input: "user@dummy-host", + fixedURI: "http://user@dummy-host/", + protocolChange: true, + shouldRunTest: () => gSingleWordDNSLookup, + }, + { + input: "user@dummy-host", + fixedURI: "http://user@dummy-host/", + keywordLookup: true, + protocolChange: true, + shouldRunTest: () => !gSingleWordDNSLookup, + }, + { + input: "user:pass@dummy-host", + fixedURI: "http://user:pass@dummy-host/", + protocolChange: true, + }, + { + input: ":pass@dummy-host", + fixedURI: "http://:pass@dummy-host/", + protocolChange: true, + }, + { + input: "user@dummy-host/path", + fixedURI: "http://user@dummy-host/path", + protocolChange: true, + shouldRunTest: () => gSingleWordDNSLookup, + }, + { + input: "jar:file:///omni.ja!/", + fixedURI: "jar:file:///omni.ja!/", + }, +]; + +if (AppConstants.platform == "win") { + testcases.push({ + input: "C:\\some\\file.txt", + fixedURI: "file:///C:/some/file.txt", + protocolChange: true, + }); + testcases.push({ + input: "//mozilla", + fixedURI: "http://mozilla/", + alternateURI: "https://www.mozilla.com/", + protocolChange: true, + }); + testcases.push({ + input: "/a", + fixedURI: "http://a/", + alternateURI: "https://www.a.com/", + keywordLookup: true, + protocolChange: true, + affectedByDNSForSingleWordHosts: true, + }); +} else { + testcases.push({ + input: "/some/file.txt", + fixedURI: "file:///some/file.txt", + protocolChange: true, + }); + testcases.push({ + input: "//mozilla", + fixedURI: "file:////mozilla", + protocolChange: true, + }); + testcases.push({ + input: "/a", + fixedURI: "file:///a", + protocolChange: true, + }); +} + +function sanitize(input) { + return input.replace(/\r|\n/g, "").trim(); +} + +add_task(async function setup() { + var prefList = [ + "browser.fixup.typo.scheme", + "keyword.enabled", + "browser.fixup.domainwhitelist.whitelisted", + "browser.fixup.domainsuffixwhitelist.test", + "browser.fixup.domainsuffixwhitelist.local.domain", + "browser.search.separatePrivateDefault", + "browser.search.separatePrivateDefault.ui.enabled", + ]; + for (let pref of prefList) { + Services.prefs.setBoolPref(pref, true); + } + + await setupSearchService(); + await addTestEngines(); + + await Services.search.setDefault( + Services.search.getEngineByName(kSearchEngineID), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + Services.search.getEngineByName(kPrivateSearchEngineID), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); + +var gSingleWordDNSLookup = false; +add_task(async function run_test() { + // Only keywordlookup things should be affected by requiring a DNS lookup for single-word hosts: + info( + "Check only keyword lookup testcases should be affected by requiring DNS for single hosts" + ); + let affectedTests = testcases.filter( + t => !t.keywordLookup && t.affectedByDNSForSingleWordHosts + ); + if (affectedTests.length) { + for (let testcase of affectedTests) { + info("Affected: " + testcase.input); + } + } + Assert.equal(affectedTests.length, 0); + await do_single_test_run(); + await do_single_test_run(true); + gSingleWordDNSLookup = true; + await do_single_test_run(); + await do_single_test_run(true); + gSingleWordDNSLookup = false; + await Services.search.setDefault( + Services.search.getEngineByName(kPostSearchEngineID), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await do_single_test_run(); + await do_single_test_run(true); +}); + +async function do_single_test_run(browserFixupAlternateEnabled = false) { + Services.prefs.setBoolPref(kForceDNSLookup, gSingleWordDNSLookup); + Services.prefs.setBoolPref(kFixupEnabled, browserFixupAlternateEnabled); + let relevantTests = gSingleWordDNSLookup + ? testcases.filter(t => t.keywordLookup) + : testcases; + + let engine = await Services.search.getDefault(); + let engineUrl = + engine.name == kPostSearchEngineID + ? kPostSearchEngineURL + : kSearchEngineURL; + let privateEngine = await Services.search.getDefaultPrivate(); + let privateEngineUrl = kPrivateSearchEngineURL; + + for (let { + input: testInput, + fixedURI: expectedFixedURI, + alternateURI: alternativeURI, + keywordLookup: expectKeywordLookup, + protocolChange: expectProtocolChange, + inWhitelist: inWhitelist, + affectedByDNSForSingleWordHosts: affectedByDNSForSingleWordHosts, + shouldRunTest, + } of relevantTests) { + // Explicitly force these into a boolean + expectKeywordLookup = !!expectKeywordLookup; + expectProtocolChange = !!expectProtocolChange; + inWhitelist = !!inWhitelist; + affectedByDNSForSingleWordHosts = !!affectedByDNSForSingleWordHosts; + + expectKeywordLookup = + expectKeywordLookup && + (!affectedByDNSForSingleWordHosts || !gSingleWordDNSLookup); + + for (let flags of flagInputs) { + info( + 'Checking "' + + testInput + + '" with flags ' + + flags + + " (DNS lookup for single words: " + + (gSingleWordDNSLookup ? "yes" : "no") + + "," + + " browser fixup alt enabled: " + + (browserFixupAlternateEnabled ? "yes" : "no") + + ")" + ); + + if (shouldRunTest && !shouldRunTest(flags)) { + continue; + } + + let URIInfo; + try { + URIInfo = Services.uriFixup.getFixupURIInfo(testInput, flags); + } catch (ex) { + // Both APIs should return an error in the same cases. + info("Caught exception: " + ex); + Assert.equal(expectedFixedURI, null); + continue; + } + + // Check the fixedURI: + let fixupFlag = browserFixupAlternateEnabled + ? Services.uriFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI + : Services.uriFixup.FIXUP_FLAG_NONE; + let forceAlternativeURI = + flags & Services.uriFixup.FIXUP_FLAG_FORCE_ALTERNATE_URI; + let makeAlternativeURI = flags & fixupFlag || forceAlternativeURI; + + if (makeAlternativeURI && alternativeURI != null) { + Assert.equal( + URIInfo.fixedURI.spec, + alternativeURI, + "should have gotten alternate URI" + ); + } else { + Assert.equal( + URIInfo.fixedURI && URIInfo.fixedURI.spec, + expectedFixedURI, + "should get correct fixed URI" + ); + } + + // Check booleans on input: + let couldDoKeywordLookup = + flags & Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + Assert.equal( + !!URIInfo.keywordProviderName, + couldDoKeywordLookup && expectKeywordLookup, + "keyword lookup as expected" + ); + + let expectProtocolChangeAfterAlternate = false; + // If alternativeURI was created, the protocol of the URI + // might have been changed to browser.fixup.alternate.protocol + // If the protocol is not the same as what was in expectedFixedURI, + // the protocol must've changed in the fixup process. + if ( + makeAlternativeURI && + alternativeURI != null && + !expectedFixedURI.startsWith(URIInfo.fixedURI.scheme) + ) { + expectProtocolChangeAfterAlternate = true; + } + + Assert.equal( + URIInfo.fixupChangedProtocol, + expectProtocolChange || expectProtocolChangeAfterAlternate, + "protocol change as expected" + ); + Assert.equal( + URIInfo.fixupCreatedAlternateURI, + makeAlternativeURI && alternativeURI != null, + "alternative URI as expected" + ); + + // Check the preferred URI + if (couldDoKeywordLookup) { + if (expectKeywordLookup) { + if (!inWhitelist) { + let urlparamInput = encodeURIComponent(sanitize(testInput)).replace( + /%20/g, + "+" + ); + // If the input starts with `?`, then URIInfo.preferredURI.spec will omit it + // In order to test this behaviour, remove `?` only if it is the first character + if (urlparamInput.startsWith("%3F")) { + urlparamInput = urlparamInput.replace("%3F", ""); + } + let isPrivate = + flags & Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT; + let searchEngineUrl = isPrivate ? privateEngineUrl : engineUrl; + let searchURL = searchEngineUrl.replace( + "{searchTerms}", + urlparamInput + ); + let spec = URIInfo.preferredURI.spec.replace(/%27/g, "'"); + Assert.equal(spec, searchURL, "should get correct search URI"); + let providerName = isPrivate ? privateEngine.name : engine.name; + Assert.equal( + URIInfo.keywordProviderName, + providerName, + "should get correct provider name" + ); + // Also check keywordToURI() uses the right engine. + let kwInfo = Services.uriFixup.keywordToURI( + urlparamInput, + isPrivate + ); + Assert.equal(kwInfo.providerName, URIInfo.providerName); + if (providerName == kPostSearchEngineID) { + Assert.ok(kwInfo.postData); + let submission = engine.getSubmission(urlparamInput); + let enginePostData = NetUtil.readInputStreamToString( + submission.postData, + submission.postData.available() + ); + let postData = NetUtil.readInputStreamToString( + kwInfo.postData, + kwInfo.postData.available() + ); + Assert.equal(postData, enginePostData); + } + } else { + Assert.equal( + URIInfo.preferredURI, + null, + "not expecting a preferred URI" + ); + } + } else { + Assert.equal( + URIInfo.preferredURI.spec, + URIInfo.fixedURI.spec, + "fixed URI should match" + ); + } + } else { + // In these cases, we should never be doing a keyword lookup and + // the fixed URI should be preferred: + let prefURI = URIInfo.preferredURI && URIInfo.preferredURI.spec; + let fixedURI = URIInfo.fixedURI && URIInfo.fixedURI.spec; + Assert.equal(prefURI, fixedURI, "fixed URI should be same as expected"); + } + Assert.equal( + sanitize(testInput), + URIInfo.originalInput, + "should mirror original input" + ); + } + } +} diff --git a/docshell/test/unit/test_URIFixup_search.js b/docshell/test/unit/test_URIFixup_search.js new file mode 100644 index 0000000000..fa1c5b38ba --- /dev/null +++ b/docshell/test/unit/test_URIFixup_search.js @@ -0,0 +1,143 @@ +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +var isWin = AppConstants.platform == "win"; + +var data = [ + { + // Valid should not be changed. + wrong: "https://example.com/this/is/a/test.html", + fixed: "https://example.com/this/is/a/test.html", + }, + { + // Unrecognized protocols should be changed. + wrong: "whatever://this/is/a/test.html", + fixed: kSearchEngineURL.replace( + "{searchTerms}", + encodeURIComponent("whatever://this/is/a/test.html") + ), + }, + + { + // Unrecognized protocols should be changed. + wrong: "whatever://this/is/a/test.html", + fixed: kPrivateSearchEngineURL.replace( + "{searchTerms}", + encodeURIComponent("whatever://this/is/a/test.html") + ), + inPrivateBrowsing: true, + }, + + // The following tests check that when a user:password is present in the URL + // `user:` isn't treated as an unknown protocol thus leaking the user and + // password to the search engine. + { + wrong: "user:pass@example.com/this/is/a/test.html", + fixed: "http://user:pass@example.com/this/is/a/test.html", + }, + { + wrong: "user@example.com:8080/this/is/a/test.html", + fixed: "http://user@example.com:8080/this/is/a/test.html", + }, + { + wrong: "https:pass@example.com/this/is/a/test.html", + fixed: "https://pass@example.com/this/is/a/test.html", + }, + { + wrong: "user:pass@example.com:8080/this/is/a/test.html", + fixed: "http://user:pass@example.com:8080/this/is/a/test.html", + }, + { + wrong: "http:user:pass@example.com:8080/this/is/a/test.html", + fixed: "http://user:pass@example.com:8080/this/is/a/test.html", + }, + { + wrong: "ttp:user:pass@example.com:8080/this/is/a/test.html", + fixed: "http://user:pass@example.com:8080/this/is/a/test.html", + }, + { + wrong: "nonsense:user:pass@example.com:8080/this/is/a/test.html", + fixed: "http://nonsense:user%3Apass@example.com:8080/this/is/a/test.html", + }, + { + wrong: "user:@example.com:8080/this/is/a/test.html", + fixed: "http://user@example.com:8080/this/is/a/test.html", + }, + { + wrong: "//user:pass@example.com:8080/this/is/a/test.html", + fixed: + (isWin ? "http:" : "file://") + + "//user:pass@example.com:8080/this/is/a/test.html", + }, + { + wrong: "://user:pass@example.com:8080/this/is/a/test.html", + fixed: "http://user:pass@example.com:8080/this/is/a/test.html", + }, + { + wrong: "localhost:8080/?param=1", + fixed: "http://localhost:8080/?param=1", + }, + { + wrong: "localhost:8080?param=1", + fixed: "http://localhost:8080/?param=1", + }, + { + wrong: "localhost:8080#somewhere", + fixed: "http://localhost:8080/#somewhere", + }, + { + wrong: "whatever://this/is/a@b/test.html", + fixed: kSearchEngineURL.replace( + "{searchTerms}", + encodeURIComponent("whatever://this/is/a@b/test.html") + ), + }, +]; + +var extProtocolSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" +].getService(Ci.nsIExternalProtocolService); + +if (extProtocolSvc && extProtocolSvc.externalProtocolHandlerExists("mailto")) { + data.push({ + wrong: "mailto:foo@bar.com", + fixed: "mailto:foo@bar.com", + }); +} + +var len = data.length; + +add_task(async function setup() { + await setupSearchService(); + await addTestEngines(); + + Services.prefs.setBoolPref("keyword.enabled", true); + Services.prefs.setBoolPref("browser.search.separatePrivateDefault", true); + Services.prefs.setBoolPref( + "browser.search.separatePrivateDefault.ui.enabled", + true + ); + + await Services.search.setDefault( + Services.search.getEngineByName(kSearchEngineID), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + Services.search.getEngineByName(kPrivateSearchEngineID), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); + +// Make sure we fix what needs fixing +add_task(function test_fix_unknown_schemes() { + for (let i = 0; i < len; ++i) { + let item = data[i]; + let flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS; + if (item.inPrivateBrowsing) { + flags |= Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT; + } + let { preferredURI } = Services.uriFixup.getFixupURIInfo(item.wrong, flags); + Assert.equal(preferredURI.spec, item.fixed); + } +}); diff --git a/docshell/test/unit/test_allowJavascript.js b/docshell/test/unit/test_allowJavascript.js new file mode 100644 index 0000000000..d1341c7bba --- /dev/null +++ b/docshell/test/unit/test_allowJavascript.js @@ -0,0 +1,291 @@ +"use strict"; + +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +const ACTOR = "AllowJavascript"; + +const HTML = String.raw`<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <script type="application/javascript"> + "use strict"; + var gFiredOnload = false; + var gFiredOnclick = false; + </script> +</head> +<body onload="gFiredOnload = true;" onclick="gFiredOnclick = true;"> +</body> +</html>`; + +const server = XPCShellContentUtils.createHttpServer({ + hosts: ["example.com", "example.org"], +}); + +server.registerPathHandler("/", (request, response) => { + response.setHeader("Content-Type", "text/html"); + response.write(HTML); +}); + +const { AllowJavascriptParent } = ChromeUtils.importESModule( + "resource://test/AllowJavascriptParent.sys.mjs" +); + +async function assertScriptsAllowed(bc, expectAllowed, desc) { + let actor = bc.currentWindowGlobal.getActor(ACTOR); + let allowed = await actor.sendQuery("CheckScriptsAllowed"); + equal( + allowed, + expectAllowed, + `Scripts should be ${expectAllowed ? "" : "dis"}allowed for ${desc}` + ); +} + +async function assertLoadFired(bc, expectFired, desc) { + let actor = bc.currentWindowGlobal.getActor(ACTOR); + let fired = await actor.sendQuery("CheckFiredLoadEvent"); + equal( + fired, + expectFired, + `Should ${expectFired ? "" : "not "}have fired load for ${desc}` + ); +} + +function createSubframe(bc, url) { + let actor = bc.currentWindowGlobal.getActor(ACTOR); + return actor.sendQuery("CreateIframe", { url }); +} + +add_task(async function () { + Services.prefs.setBoolPref("dom.security.https_first", false); + ChromeUtils.registerWindowActor(ACTOR, { + allFrames: true, + child: { + esModuleURI: "resource://test/AllowJavascriptChild.sys.mjs", + events: { load: { capture: true } }, + }, + parent: { + esModuleURI: "resource://test/AllowJavascriptParent.sys.mjs", + }, + }); + + let page = await XPCShellContentUtils.loadContentPage("http://example.com/", { + remote: true, + remoteSubframes: true, + }); + + let bc = page.browsingContext; + + { + let oopFrame1 = await createSubframe(bc, "http://example.org/"); + let inprocFrame1 = await createSubframe(bc, "http://example.com/"); + + let oopFrame1OopSub = await createSubframe( + oopFrame1, + "http://example.com/" + ); + let inprocFrame1OopSub = await createSubframe( + inprocFrame1, + "http://example.org/" + ); + + equal( + oopFrame1.allowJavascript, + true, + "OOP BC should inherit allowJavascript from parent" + ); + equal( + inprocFrame1.allowJavascript, + true, + "In-process BC should inherit allowJavascript from parent" + ); + equal( + oopFrame1OopSub.allowJavascript, + true, + "OOP BC child should inherit allowJavascript from parent" + ); + equal( + inprocFrame1OopSub.allowJavascript, + true, + "In-process child BC should inherit allowJavascript from parent" + ); + + await assertLoadFired(bc, true, "top BC"); + await assertScriptsAllowed(bc, true, "top BC"); + + await assertLoadFired(oopFrame1, true, "OOP frame 1"); + await assertScriptsAllowed(oopFrame1, true, "OOP frame 1"); + + await assertLoadFired(inprocFrame1, true, "In-process frame 1"); + await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1"); + + await assertLoadFired(oopFrame1OopSub, true, "OOP frame 1 subframe"); + await assertScriptsAllowed(oopFrame1OopSub, true, "OOP frame 1 subframe"); + + await assertLoadFired( + inprocFrame1OopSub, + true, + "In-process frame 1 subframe" + ); + await assertScriptsAllowed( + inprocFrame1OopSub, + true, + "In-process frame 1 subframe" + ); + + bc.allowJavascript = false; + await assertScriptsAllowed(bc, false, "top BC with scripts disallowed"); + await assertScriptsAllowed( + oopFrame1, + false, + "OOP frame 1 with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame1, + false, + "In-process frame 1 with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + oopFrame1OopSub, + false, + "OOP frame 1 subframe with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame1OopSub, + false, + "In-process frame 1 subframe with top BC with scripts disallowed" + ); + + let oopFrame2 = await createSubframe(bc, "http://example.org/"); + let inprocFrame2 = await createSubframe(bc, "http://example.com/"); + + equal( + oopFrame2.allowJavascript, + false, + "OOP BC 2 should inherit allowJavascript from parent" + ); + equal( + inprocFrame2.allowJavascript, + false, + "In-process BC 2 should inherit allowJavascript from parent" + ); + + await assertLoadFired( + oopFrame2, + undefined, + "OOP frame 2 with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + oopFrame2, + false, + "OOP frame 2 with top BC with scripts disallowed" + ); + await assertLoadFired( + inprocFrame2, + undefined, + "In-process frame 2 with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame2, + false, + "In-process frame 2 with top BC with scripts disallowed" + ); + + bc.allowJavascript = true; + await assertScriptsAllowed(bc, true, "top BC"); + + await assertScriptsAllowed(oopFrame1, true, "OOP frame 1"); + await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1"); + await assertScriptsAllowed(oopFrame1OopSub, true, "OOP frame 1 subframe"); + await assertScriptsAllowed( + inprocFrame1OopSub, + true, + "In-process frame 1 subframe" + ); + + await assertScriptsAllowed(oopFrame2, false, "OOP frame 2"); + await assertScriptsAllowed(inprocFrame2, false, "In-process frame 2"); + + oopFrame1.currentWindowGlobal.allowJavascript = false; + inprocFrame1.currentWindowGlobal.allowJavascript = false; + + await assertScriptsAllowed( + oopFrame1, + false, + "OOP frame 1 with second level WC scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame1, + false, + "In-process frame 1 with second level WC scripts disallowed" + ); + await assertScriptsAllowed( + oopFrame1OopSub, + false, + "OOP frame 1 subframe second level WC scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame1OopSub, + false, + "In-process frame 1 subframe with second level WC scripts disallowed" + ); + + oopFrame1.reload(0); + inprocFrame1.reload(0); + await Promise.all([ + AllowJavascriptParent.promiseLoad(oopFrame1), + AllowJavascriptParent.promiseLoad(inprocFrame1), + ]); + + equal( + oopFrame1.currentWindowGlobal.allowJavascript, + true, + "WindowContext.allowJavascript does not persist after navigation for OOP frame 1" + ); + equal( + inprocFrame1.currentWindowGlobal.allowJavascript, + true, + "WindowContext.allowJavascript does not persist after navigation for in-process frame 1" + ); + + await assertScriptsAllowed(oopFrame1, true, "OOP frame 1"); + await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1"); + } + + bc.allowJavascript = false; + + bc.reload(0); + await AllowJavascriptParent.promiseLoad(bc); + + await assertLoadFired( + bc, + undefined, + "top BC with scripts disabled after reload" + ); + await assertScriptsAllowed( + bc, + false, + "top BC with scripts disabled after reload" + ); + + await page.loadURL("http://example.org/?other"); + bc = page.browsingContext; + + await assertLoadFired( + bc, + undefined, + "top BC with scripts disabled after navigation" + ); + await assertScriptsAllowed( + bc, + false, + "top BC with scripts disabled after navigation" + ); + + await page.close(); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/docshell/test/unit/test_browsing_context_structured_clone.js b/docshell/test/unit/test_browsing_context_structured_clone.js new file mode 100644 index 0000000000..1c91a9b0b2 --- /dev/null +++ b/docshell/test/unit/test_browsing_context_structured_clone.js @@ -0,0 +1,70 @@ +"use strict"; + +add_task(async function test_BrowsingContext_structured_clone() { + let browser = Services.appShell.createWindowlessBrowser(false); + + let frame = browser.document.createElement("iframe"); + + await new Promise(r => { + frame.onload = () => r(); + browser.document.body.appendChild(frame); + }); + + let { browsingContext } = frame; + + let sch = new StructuredCloneHolder("debug name", "<anonymized> debug name", { + browsingContext, + }); + + let deserialize = () => sch.deserialize({}, true); + + // Check that decoding a live browsing context produces the correct + // object. + equal( + deserialize().browsingContext, + browsingContext, + "Got correct browsing context from StructuredClone deserialize" + ); + + // Check that decoding a second time still succeeds. + equal( + deserialize().browsingContext, + browsingContext, + "Got correct browsing context from second StructuredClone deserialize" + ); + + // Destroy the browsing context and make sure that the decode fails + // with a DataCloneError. + // + // Making sure the BrowsingContext is actually destroyed by the time + // we do the second decode is a bit tricky. We obviously have clear + // our local references to it, and give the GC a chance to reap them. + // And we also, of course, have to destroy the frame that it belongs + // to, or its frame loader and window global would hold it alive. + // + // Beyond that, we don't *have* to reload or destroy the parent + // document, but we do anyway just to be safe. + // + + frame.remove(); + frame = null; + browsingContext = null; + + browser.document.location.reload(); + browser.close(); + + // We will schedule a precise GC and do both GC and CC a few times, to make + // sure we have completely destroyed the WindowGlobal actors (which keep + // references to their BrowsingContexts) in order + // to allow their (now snow-white) references to be collected. + await schedulePreciseGCAndForceCC(3); + + // OK. We can be fairly confident that the BrowsingContext object + // stored in our structured clone data has been destroyed. Make sure + // that attempting to decode it again leads to the appropriate error. + Assert.throws( + deserialize, + e => e.name === "DataCloneError", + "Should get a DataCloneError when trying to decode a dead BrowsingContext" + ); +}); diff --git a/docshell/test/unit/test_bug442584.js b/docshell/test/unit/test_bug442584.js new file mode 100644 index 0000000000..c109557f50 --- /dev/null +++ b/docshell/test/unit/test_bug442584.js @@ -0,0 +1,35 @@ +var prefetch = Cc["@mozilla.org/prefetch-service;1"].getService( + Ci.nsIPrefetchService +); + +var ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +function run_test() { + // Fill up the queue + Services.prefs.setBoolPref("network.prefetch-next", true); + for (var i = 0; i < 5; i++) { + var uri = Services.io.newURI("http://localhost/" + i); + var referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); + prefetch.prefetchURI(uri, referrerInfo, null, true); + } + + // Make sure the queue has items in it... + Assert.ok(prefetch.hasMoreElements()); + + // Now disable the pref to force the queue to empty... + Services.prefs.setBoolPref("network.prefetch-next", false); + Assert.ok(!prefetch.hasMoreElements()); + + // Now reenable the pref, and add more items to the queue. + Services.prefs.setBoolPref("network.prefetch-next", true); + for (var k = 0; k < 5; k++) { + var uri2 = Services.io.newURI("http://localhost/" + k); + var referrerInfo2 = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri2); + prefetch.prefetchURI(uri2, referrerInfo2, null, true); + } + Assert.ok(prefetch.hasMoreElements()); +} diff --git a/docshell/test/unit/test_pb_notification.js b/docshell/test/unit/test_pb_notification.js new file mode 100644 index 0000000000..51cd3b95ff --- /dev/null +++ b/docshell/test/unit/test_pb_notification.js @@ -0,0 +1,18 @@ +function destroy_transient_docshell() { + let windowlessBrowser = Services.appShell.createWindowlessBrowser(true); + windowlessBrowser.docShell.setOriginAttributes({ privateBrowsingId: 1 }); + windowlessBrowser.close(); + do_test_pending(); + do_timeout(0, Cu.forceGC); +} + +function run_test() { + var obs = { + observe(aSubject, aTopic, aData) { + Assert.equal(aTopic, "last-pb-context-exited"); + do_test_finished(); + }, + }; + Services.obs.addObserver(obs, "last-pb-context-exited"); + destroy_transient_docshell(); +} diff --git a/docshell/test/unit/test_privacy_transition.js b/docshell/test/unit/test_privacy_transition.js new file mode 100644 index 0000000000..ae1bf71284 --- /dev/null +++ b/docshell/test/unit/test_privacy_transition.js @@ -0,0 +1,21 @@ +var gNotifications = 0; + +var observer = { + QueryInterface: ChromeUtils.generateQI([ + "nsIPrivacyTransitionObserver", + "nsISupportsWeakReference", + ]), + + privateModeChanged(enabled) { + gNotifications++; + }, +}; + +function run_test() { + let windowlessBrowser = Services.appShell.createWindowlessBrowser(true); + windowlessBrowser.docShell.addWeakPrivacyTransitionObserver(observer); + windowlessBrowser.docShell.setOriginAttributes({ privateBrowsingId: 1 }); + windowlessBrowser.docShell.setOriginAttributes({ privateBrowsingId: 0 }); + windowlessBrowser.close(); + Assert.equal(gNotifications, 2); +} diff --git a/docshell/test/unit/test_subframe_stop_after_parent_error.js b/docshell/test/unit/test_subframe_stop_after_parent_error.js new file mode 100644 index 0000000000..2f80d18ebb --- /dev/null +++ b/docshell/test/unit/test_subframe_stop_after_parent_error.js @@ -0,0 +1,146 @@ +"use strict"; +// Tests that pending subframe requests for an initial about:blank +// document do not delay showing load errors (and possibly result in a +// crash at docShell destruction) for failed document loads. + +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally(/undefined/); + +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +const server = XPCShellContentUtils.createHttpServer({ + hosts: ["example.com"], +}); + +// Registers a URL with the HTTP server which will not return a response +// until we're ready to. +function registerSlowPage(path) { + let result = { + url: `http://example.com/${path}`, + }; + + let finishedPromise = new Promise(resolve => { + result.finish = resolve; + }); + + server.registerPathHandler(`/${path}`, async (request, response) => { + response.processAsync(); + + response.setHeader("Content-Type", "text/html"); + response.write("<html><body>Hello.</body></html>"); + + await finishedPromise; + + response.finish(); + }); + + return result; +} + +let topFrameRequest = registerSlowPage("top.html"); +let subFrameRequest = registerSlowPage("frame.html"); + +let thunks = new Set(); +function promiseStateStop(webProgress) { + return new Promise(resolve => { + let listener = { + onStateChange(aWebProgress, request, stateFlags, status) { + if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + webProgress.removeProgressListener(listener); + + thunks.delete(listener); + resolve(); + } + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + }; + + // Keep the listener alive, since it's stored as a weak reference. + thunks.add(listener); + webProgress.addProgressListener( + listener, + Ci.nsIWebProgress.NOTIFY_STATE_REQUEST + ); + }); +} + +async function runTest(waitForErrorPage) { + let page = await XPCShellContentUtils.loadContentPage("about:blank", { + remote: false, + }); + let { browser } = page; + + // Watch for the HTTP request for the top frame so that we can cancel + // it with an error. + let requestPromise = TestUtils.topicObserved( + "http-on-modify-request", + subject => subject.QueryInterface(Ci.nsIRequest).name == topFrameRequest.url + ); + + // Create a frame with a source URL which will not return a response + // before we cancel it with an error. + let doc = browser.contentDocument; + let frame = doc.createElement("iframe"); + frame.src = topFrameRequest.url; + doc.body.appendChild(frame); + + // Create a subframe in the initial about:blank document for the above + // frame which will not return a response before we cancel the + // document request. + let frameDoc = frame.contentDocument; + let subframe = frameDoc.createElement("iframe"); + subframe.src = subFrameRequest.url; + frameDoc.body.appendChild(subframe); + + let [req] = await requestPromise; + + info("Cancel request for parent frame"); + req.cancel(Cr.NS_ERROR_PROXY_CONNECTION_REFUSED); + + // Request cancelation is not synchronous, so wait for the STATE_STOP + // event to fire. + await promiseStateStop( + browser.docShell.nsIInterfaceRequestor.getInterface(Ci.nsIWebProgress) + ); + + // And make a trip through the event loop to give the DocLoader a + // chance to update its state. + await new Promise(executeSoon); + + if (waitForErrorPage) { + // Make sure that canceling the request with an error code actually + // shows an error page without waiting for a subframe response. + await TestUtils.waitForCondition(() => + frame.contentDocument.documentURI.startsWith("about:neterror?") + ); + } + + info("Remove frame"); + frame.remove(); + + await page.close(); +} + +add_task(async function testRemoveFrameImmediately() { + await runTest(false); +}); + +add_task(async function testRemoveFrameAfterErrorPage() { + await runTest(true); +}); + +add_task(async function () { + // Allow the document requests for the frames to complete. + topFrameRequest.finish(); + subFrameRequest.finish(); +}); diff --git a/docshell/test/unit/xpcshell.ini b/docshell/test/unit/xpcshell.ini new file mode 100644 index 0000000000..8bd7ef4f28 --- /dev/null +++ b/docshell/test/unit/xpcshell.ini @@ -0,0 +1,35 @@ +[DEFAULT] +head = head_docshell.js +support-files = + data/engine.xml + data/enginePost.xml + data/enginePrivate.xml + +[test_allowJavascript.js] +skip-if = os == 'android' +support-files = + AllowJavascriptChild.sys.mjs + AllowJavascriptParent.sys.mjs +[test_bug442584.js] +[test_browsing_context_structured_clone.js] +[test_URIFixup.js] +[test_URIFixup_check_host.js] +[test_URIFixup_external_protocol_fallback.js] +skip-if = os == 'android' +[test_URIFixup_forced.js] +# Disabled for 1563343 -- URI fixup should be done at the app level in GV. +skip-if = os == 'android' +[test_URIFixup_search.js] +skip-if = os == 'android' +[test_URIFixup_info.js] +skip-if = + os == 'android' + win10_2004 && debug # Bug 1727925 +[test_pb_notification.js] +# Bug 751575: unrelated JS changes cause timeouts on random platforms +skip-if = true +[test_privacy_transition.js] +[test_subframe_stop_after_parent_error.js] +skip-if = + os == 'android' + appname == 'thunderbird' # Needs to run without E10s, can't do that. diff --git a/docshell/test/unit_ipc/test_pb_notification_ipc.js b/docshell/test/unit_ipc/test_pb_notification_ipc.js new file mode 100644 index 0000000000..6408e7753e --- /dev/null +++ b/docshell/test/unit_ipc/test_pb_notification_ipc.js @@ -0,0 +1,15 @@ +function run_test() { + var notifications = 0; + var obs = { + observe(aSubject, aTopic, aData) { + Assert.equal(aTopic, "last-pb-context-exited"); + notifications++; + }, + }; + Services.obs.addObserver(obs, "last-pb-context-exited"); + + run_test_in_child("../unit/test_pb_notification.js", function () { + Assert.equal(notifications, 1); + do_test_finished(); + }); +} diff --git a/docshell/test/unit_ipc/xpcshell.ini b/docshell/test/unit_ipc/xpcshell.ini new file mode 100644 index 0000000000..30e98a270f --- /dev/null +++ b/docshell/test/unit_ipc/xpcshell.ini @@ -0,0 +1,7 @@ +[DEFAULT] +head = +skip-if = toolkit == 'android' + +[test_pb_notification_ipc.js] +# Bug 751575: Perma-fails with: command timed out: 1200 seconds without output +skip-if = true |