diff options
Diffstat (limited to '')
796 files changed, 81726 insertions, 0 deletions
diff --git a/docshell/base/BaseHistory.cpp b/docshell/base/BaseHistory.cpp new file mode 100644 index 0000000000..3932711b5b --- /dev/null +++ b/docshell/base/BaseHistory.cpp @@ -0,0 +1,247 @@ +/* -*- 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, "cached-favicon"_ns, + "chrome"_ns, "data"_ns, "imap"_ns, + "javascript"_ns, "mailbox"_ns, "news"_ns, + "page-icon"_ns, "resource"_ns, "view-source"_ns, + "moz-extension"_ns, "moz-page-thumb"_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..141036a86c --- /dev/null +++ b/docshell/base/BrowsingContext.cpp @@ -0,0 +1,3905 @@ +/* -*- 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 "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 "nsGlobalWindowInner.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 (aType == Type::Chrome && !aParent) { + // See SetOpener for why we do this inheritance. + fields.Get<IDX_PrefersColorSchemeOverride>() = + aOpener->Top()->GetPrefersColorSchemeOverride(); + } + } + + 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 || aType == Type::Content) { + // Non-root browsing-contexts inherit their status from its parent. + // Top-content either gets managed by the top chrome, or gets manually + // managed by the front-end (see ManuallyManagesActiveness). In any case + // we want to start off as inactive. + return ExplicitActiveStatus::None; + } + // 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_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(); + ClearCachedValuesOfLocations(); +} + +// 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. + return Canonical() + ->TopCrossChromeBoundary() + ->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() && aEmbedder->IsXULElement() && IsTopContent()) { + nsAutoString messageManagerGroup; + aEmbedder->GetAttr(nsGkAtoms::messagemanagergroup, messageManagerGroup); + txn.SetMessageManagerGroup(messageManagerGroup); + txn.SetUseGlobalHistory( + !aEmbedder->HasAttr(nsGkAtoms::disableglobalhistory)); + if (!aEmbedder->HasAttr(nsGkAtoms::manualactiveness)) { + // We're active iff the parent cross-chrome-boundary is active. Note we + // can't just use this->Canonical()->GetParentCrossChromeBoundary here, + // since mEmbedderElement is still null at this point. + RefPtr bc = aEmbedder->OwnerDoc()->GetBrowsingContext(); + const bool isActive = bc && bc->IsActive(); + txn.SetExplicitActive(isActive ? ExplicitActiveStatus::Active + : ExplicitActiveStatus::Inactive); + if (auto* bp = Canonical()->GetBrowserParent()) { + bp->SetRenderLayers(isActive); + } + } + } + + 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 (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(); + + ClearCachedValuesOfLocations(); + + // 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(); +} + +void BrowsingContext::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)); + + if (IsChrome() && IsTop() && aOpener) { + // Inherit color scheme overrides from parent window. This is to inherit the + // color scheme of dark themed PBM windows in dialogs opened by such + // windows. + auto openerOverride = aOpener->Top()->PrefersColorSchemeOverride(); + if (openerOverride != PrefersColorSchemeOverride()) { + MOZ_ALWAYS_SUCCEEDS(SetPrefersColorSchemeOverride(openerOverride)); + } + } +} + +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); + + ClearCachedValuesOfLocations(); + mLocations.clear(); +} + +/* 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"); + aLoadState->AssertProcessCouldTriggerLoadIfSystem(); + + 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"); + aLoadState->AssertProcessCouldTriggerLoadIfSystem(); + + 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); + } +} + +template <typename FuncT> +inline bool ApplyToDocumentsForPopup(Document* doc, FuncT func) { + // 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. + if (func(doc)) { + return true; + } + if (!doc->IsInitialDocument()) { + return false; + } + Document* parentDoc = doc->GetInProcessParentDocument(); + if (!parentDoc || !parentDoc->NodePrincipal()->Equals(doc->NodePrincipal())) { + return false; + } + return func(parentDoc); +} + +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) { + auto ConsumeTransientUserActivationForMultiplePopupBlocking = + [&]() -> bool { + return ApplyToDocumentsForPopup(doc, [](Document* doc) { + return doc->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::GetUserActivationModifiersForPopup( + UserActivation::Modifiers* aModifiers) { + RefPtr<Document> doc = GetExtantDocument(); + if (doc) { + // Unlike RevisePopupAbuseLevel, modifiers can always be used regardless + // of PopupControlState. + (void)ApplyToDocumentsForPopup(doc, [&](Document* doc) { + return doc->GetTransientUserGestureActivationModifiers(aModifiers); + }); + } +} + +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()) { + 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"); +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_ExplicitActive>, + const ExplicitActiveStatus&, + ContentParent* aSource) { + return XRE_IsParentProcess() && IsTop() && !aSource; +} + +void BrowsingContext::DidSet(FieldIndex<IDX_ExplicitActive>, + ExplicitActiveStatus aOldValue) { + MOZ_ASSERT(IsTop()); + + const bool isActive = IsActive(); + const bool wasActive = [&] { + if (aOldValue != ExplicitActiveStatus::None) { + return aOldValue == ExplicitActiveStatus::Active; + } + return GetParent() && GetParent()->IsActive(); + }(); + + if (isActive == wasActive) { + return; + } + + Group()->UpdateToplevelsSuspendedIfNeeded(); + if (XRE_IsParentProcess()) { + if (BrowserParent* bp = Canonical()->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 + } + + // NOTE(emilio): Ideally we'd want to reuse the ExplicitActiveStatus::None + // set-up, but that's non-trivial to do because in content processes we + // can't access the top-cross-chrome-boundary bc. + auto manageTopDescendant = [&](auto* aChild) { + if (!aChild->ManuallyManagesActiveness()) { + aChild->SetIsActiveInternal(isActive, IgnoreErrors()); + if (BrowserParent* bp = aChild->GetBrowserParent()) { + bp->SetRenderLayers(isActive); + } + } + return CallState::Continue; + }; + Canonical()->CallOnAllTopDescendants(manageTopDescendant, + /* aIncludeNestedBrowsers = */ false); + } + + 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, nsGlobalWindowInner::Cast(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); +} + +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 (IsTop()) { + 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_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; +} + +bool BrowsingContext::CanSet(FieldIndex<IDX_ForceOffline>, bool aNewValue, + ContentParent* aSource) { + return XRE_IsParentProcess() && !aSource; +} + +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 (auto* bc = BrowserChild::GetFrom(shell)) { + bc->UpdateVisibility(); + } + + 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(); +} + +void BrowsingContext::LocationCreated(dom::Location* aLocation) { + MOZ_ASSERT(!aLocation->isInList()); + mLocations.insertBack(aLocation); +} + +void BrowsingContext::ClearCachedValuesOfLocations() { + for (dom::Location* loc = mLocations.getFirst(); loc; loc = loc->getNext()) { + loc->ClearCachedValues(); + } +} + +} // 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..5ec95a61e4 --- /dev/null +++ b/docshell/base/BrowsingContext.h @@ -0,0 +1,1469 @@ +/* -*- 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; +class Location; +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(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 nsIDocumentViewer::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) \ + /* If true, this browsing context is offline */ \ + FIELD(ForceOffline, 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); + 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; + bool ForceOffline() const { return GetForceOffline(); } + + 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); + + // 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 RevisePopupAbuseLevel( + PopupBlocker::PopupControlState aControl); + + // Get the modifiers associated with the user activation for relevant + // documents. The window.open code uses this routine to determine where the + // new window should be located. + void GetUserActivationModifiersForPopup( + UserActivation::Modifiers* aModifiers); + + 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(); + } + + void LocationCreated(dom::Location* aLocation); + void ClearCachedValuesOfLocations(); + + 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)); + } + + 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); + + bool CanSet(FieldIndex<IDX_ExplicitActive>, const ExplicitActiveStatus&, + ContentParent* aSource); + 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); + 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_ForceOffline>, bool aNewValue, + 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; + + mozilla::LinkedList<dom::Location> mLocations; +}; + +/** + * 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. We assume that in this case the failure is unrecoverable, so we + * crash immediately rather than return false. + */ +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..622ed2a6c8 --- /dev/null +++ b/docshell/base/BrowsingContextGroup.cpp @@ -0,0 +1,570 @@ +/* -*- 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::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(nsIRunnable* aRunnable) { + MOZ_ASSERT(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); + } + + return mPostMessageEventQueue->Dispatch(aRunnable, NS_DISPATCH_NORMAL); +} + +void BrowsingContextGroup::FlushPostMessageEvents() { + if (!mPostMessageEventQueue) { + return; + } + 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 mozilla::dom diff --git a/docshell/base/BrowsingContextGroup.h b/docshell/base/BrowsingContextGroup.h new file mode 100644 index 0000000000..2f8649da2d --- /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(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..7c915bd666 --- /dev/null +++ b/docshell/base/BrowsingContextWebProgress.cpp @@ -0,0 +1,443 @@ +/* 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/AlreadyAddRefed.h" +#include "mozilla/BounceTrackingState.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" +#include "mozilla/RefPtr.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; +} + +already_AddRefed<BounceTrackingState> +BrowsingContextWebProgress::GetBounceTrackingState() { + if (!mBounceTrackingState) { + mBounceTrackingState = BounceTrackingState::GetOrCreate(this); + } + return do_AddRef(mBounceTrackingState); +} + +//////////////////////////////////////////////////////////////////////////////// +// 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..81610886b9 --- /dev/null +++ b/docshell/base/BrowsingContextWebProgress.h @@ -0,0 +1,103 @@ +/* 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" +#include "mozilla/BounceTrackingState.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; } + + already_AddRefed<BounceTrackingState> GetBounceTrackingState(); + + 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; + + RefPtr<mozilla::BounceTrackingState> mBounceTrackingState; +}; + +} // 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..33ed5aab28 --- /dev/null +++ b/docshell/base/CanonicalBrowsingContext.cpp @@ -0,0 +1,3104 @@ +/* -*- 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/Promise.h" +#include "mozilla/dom/Promise-inl.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" +#include "mozilla/layers/CompositorBridgeChild.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 "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::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()); + txn.SetForceOffline(GetForceOffline()); + + // 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()); + + // When using site-specific zoom, we let the front-end manage it, otherwise it + // can cause weirdness like bug 1846141. + if (!StaticPrefs::browser_zoom_siteSpecific()) { + txn.SetFullZoom(GetFullZoom()); + txn.SetTextZoom(GetTextZoom()); + } + + // 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); + } + + 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(); + nsGlobalWindowOuter* topWin; + if ((topWin = nsGlobalWindowOuter::Cast(chromeTop->GetDOMWindow())) && + topWin->IsChromeWindow()) { + return do_AddRef(topWin->GetBrowserDOMWindow()); + } + return nullptr; +} + +already_AddRefed<WindowGlobalParent> +CanonicalBrowsingContext::GetEmbedderWindowGlobal() const { + uint64_t windowId = GetEmbedderInnerWindowId(); + if (windowId == 0) { + return nullptr; + } + + return WindowGlobalParent::GetByInnerWindowId(windowId); +} + +CanonicalBrowsingContext* +CanonicalBrowsingContext::GetParentCrossChromeBoundary() { + if (GetParent()) { + return Cast(GetParent()); + } + if (auto* embedder = GetEmbedderElement()) { + return Cast(embedder->OwnerDoc()->GetBrowsingContext()); + } + return nullptr; +} + +CanonicalBrowsingContext* CanonicalBrowsingContext::TopCrossChromeBoundary() { + CanonicalBrowsingContext* bc = this; + while (auto* parent = bc->GetParentCrossChromeBoundary()) { + bc = parent; + } + return bc; +} + +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 FunctionRef<CallState(CanonicalBrowsingContext*)>& aCallback, + bool aIncludeNestedBrowsers) { + MOZ_ASSERT(IsTop(), "Should only call on top BC"); + MOZ_ASSERT( + !aIncludeNestedBrowsers || + (IsChrome() && !GetParentCrossChromeBoundary()), + "If aIncludeNestedBrowsers is set, should only call on top chrome BC"); + + if (!IsInProcess()) { + // We rely on top levels having to be embedded in the parent process, so + // we can only have top level descendants if embedded here.. + return; + } + + AutoTArray<RefPtr<BrowsingContextGroup>, 32> 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; + } + + if (aIncludeNestedBrowsers) { + if (this != bc->Canonical()->TopCrossChromeBoundary()) { + continue; + } + } else { + auto* parent = bc->Canonical()->GetParentCrossChromeBoundary(); + if (!parent || this != parent->Top()) { + continue; + } + } + + 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 EvictDocumentViewersOrReplaceEntry. + 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 EvictDocumentViewersOrReplaceEntry. +} + +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 wasAlreadyActive = IsActive(); + + nsCOMPtr<nsIWidget> widget; + if (auto* docShell = GetDocShell()) { + nsDocShell::Cast(docShell)->GetMainWidget(getter_AddRefs(widget)); + } + + Unused << NS_WARN_IF(!widget); + const bool isNowActive = + ForceAppWindowActive() || (widget && !widget->IsFullyOccluded() && + widget->SizeMode() != nsSizeMode_Minimized); + + if (isNowActive == wasAlreadyActive) { + return; + } + + SetIsActiveInternal(isNowActive, IgnoreErrors()); + if (widget) { + // Pause if we are not active, resume if we are active. + widget->PauseOrResumeCompositor(!isNowActive); + } +} + +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); + + if (aBrowserParent && IsTopContent() && !ManuallyManagesActiveness()) { + aBrowserParent->SetRenderLayers(IsActive()); + } + + mCurrentBrowserParent = aBrowserParent; +} + +bool CanonicalBrowsingContext::ManuallyManagesActiveness() const { + auto* el = GetEmbedderElement(); + return el && el->IsXULElement() && el->HasAttr(nsGkAtoms::manualactiveness); +} + +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; + blocker->AddCallbacksWithCycleCollectedArgs( + [change](JSContext*, JS::Handle<JS::Value>, ErrorResult&) { + change->mWaitingForPrepareToChange = false; + change->MaybeFinish(); + }, + [change](JSContext*, JS::Handle<JS::Value> aValue, ErrorResult&) { + change->Cancel( + Promise::TryExtractNSResultFromRejectionValue(aValue)); + }); + } + } + + // 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->EvictOutOfRangeDocumentViewers(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(); +} + +already_AddRefed<BounceTrackingState> +CanonicalBrowsingContext::GetBounceTrackingState() { + if (!mWebProgress) { + return nullptr; + } + return mWebProgress->GetBounceTrackingState(); +} + +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 mozilla::dom diff --git a/docshell/base/CanonicalBrowsingContext.h b/docshell/base/CanonicalBrowsingContext.h new file mode 100644 index 0000000000..132c9f2157 --- /dev/null +++ b/docshell/base/CanonicalBrowsingContext.h @@ -0,0 +1,620 @@ +/* -*- 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; +class BounceTrackingState; + +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; + + CanonicalBrowsingContext* GetParentCrossChromeBoundary(); + CanonicalBrowsingContext* TopCrossChromeBoundary(); + Nullable<WindowProxyHolder> GetTopChromeWindow(); + + nsISHistory* GetSessionHistory(); + SessionHistoryEntry* GetActiveSessionHistoryEntry(); + void SetActiveSessionHistoryEntry(SessionHistoryEntry* aEntry); + + bool ManuallyManagesActiveness() const; + + 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. + // + // If aIncludeNestedBrowsers is true, then all top descendants are included, + // even those inside a nested top browser. + void CallOnAllTopDescendants( + const FunctionRef<CallState(CanonicalBrowsingContext*)>& aCallback, + bool aIncludeNestedBrowsers); + + 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 SetIsActive(bool aIsActive, ErrorResult& aRv) { + MOZ_ASSERT(ManuallyManagesActiveness(), + "Shouldn't be setting active status of this browsing context if " + "not manually managed"); + SetIsActiveInternal(aIsActive, aRv); + } + + void SetIsActiveInternal(bool aIsActive, ErrorResult& aRv) { + SetExplicitActive(aIsActive ? ExplicitActiveStatus::Active + : ExplicitActiveStatus::Inactive, + aRv); + } + + 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(); + + already_AddRefed<BounceTrackingState> GetBounceTrackingState(); + + 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..30da778f45 --- /dev/null +++ b/docshell/base/IHistory.h @@ -0,0 +1,175 @@ +/* -*- 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, + /** + * If REDIRECT_SOURCE is set, this indicates that this is a special redirect + * caused by HSTS or HTTPS-Only/First upgrading to the HTTPS version of the + * URI. + */ + REDIRECT_SOURCE_UPGRADED = 1 << 6 + }; + + /** + * 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..e386e75b35 --- /dev/null +++ b/docshell/base/SyncedContextInlines.h @@ -0,0 +1,359 @@ +/* -*- 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): "); + // data-review+ at https://bugzilla.mozilla.org/show_bug.cgi?id=1618992#c7 + return IPC_FAIL_UNSAFE_PRINTF(aSource, "%s", 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..6fde38a310 --- /dev/null +++ b/docshell/base/URIFixup.sys.mjs @@ -0,0 +1,1306 @@ +/* -*- 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, + "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, +} = 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. +ChromeUtils.defineLazyGetter( + lazy, + "userPasswordRegex", + () => /^([a-z+.-]+:\/{0,3})*([^\/@]+@).+/i +); + +// Regex used to identify the string that starts with port expression. +ChromeUtils.defineLazyGetter(lazy, "portRegex", () => /^:\d{1,5}([?#/]|$)/); + +// Regex used to identify numbers. +ChromeUtils.defineLazyGetter(lazy, "numberRegex", () => /^[0-9]+(\.[0-9]+)?$/); + +// Regex used to identify tab separated content (having at least 2 tabs). +ChromeUtils.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. +ChromeUtils.defineLazyGetter( + lazy, + "possiblyHostPortRegex", + () => /^[a-z0-9-]+(\.[a-z0-9-]+)*:[0-9]{1,5}([/?#]|$)/i +); + +// Regex used to strip newlines. +ChromeUtils.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. +ChromeUtils.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. +ChromeUtils.defineLazyGetter( + lazy, + "IPv4LikeRegex", + () => /^(?:[a-z+.-]+:\/*(?!\/))?(?:\d{1,3}\.){2,3}\d{1,3}(?::\d+|\/)?/i +); +ChromeUtils.defineLazyGetter( + lazy, + "IPv6LikeRegex", + () => + /^(?:[a-z+.-]+:\/*(?!\/))?\[(?:[0-9a-f]{0,4}:){0,7}[0-9a-f]{0,4}\]?(?::\d+|\/)?/i +); + +// Regex used to detect spaces in URL credentials. +ChromeUtils.defineLazyGetter( + lazy, + "DetectSpaceInCredentialsRegex", + () => /^[^/]*\s[^/]*@/ +); + +// Cache of known domains. +ChromeUtils.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. +ChromeUtils.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_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; + let untrimmedURIString = uriString; + + // 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) && + !lazy.DetectSpaceInCredentialsRegex.test(untrimmedURIString) + ) { + let uriWithProtocol = fixupURIProtocol(uriString); + if (uriWithProtocol) { + info.fixedURI = uriWithProtocol; + info.fixupChangedProtocol = true; + info.wasSchemelessInput = 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 wasSchemelessInput(changed) { + this._wasSchemelessInput = changed; + }, + get wasSchemelessInput() { + return !!this._wasSchemelessInput; + }, + + 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; + if ( + !(fixupFlags & FIXUP_FLAGS_MAKE_ALTERNATE_URI) || + // 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 "/" or "~". + attemptFixup = /^[~/]/.test(uriString); + } + 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; + 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..d2032bc551 --- /dev/null +++ b/docshell/base/WindowContext.cpp @@ -0,0 +1,697 @@ +/* -*- 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/RFPTargetIPCUtils.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/ClearOnShutdown.h" +#include "nsGlobalWindowInner.h" +#include "nsIScriptError.h" +#include "nsIWebProgressListener.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 (!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) { + 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_UsingStorageAccess>, + const bool& aUsingStorageAccess, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_ShouldResistFingerprinting>, + const bool& aShouldResistFingerprinting, + ContentParent* aSource) { + return CheckOnlyOwningProcessCanSet(aSource); +} + +bool WindowContext::CanSet(FieldIndex<IDX_OverriddenFingerprintingSettings>, + const Maybe<RFPTarget>& aValue, + 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_UserActivationStateAndModifiers>) { + MOZ_ASSERT_IF(!IsInProcess(), mUserGestureStart.IsNull()); + USER_ACTIVATION_LOG("Set user gesture activation 0x%02" PRIu8 + " for %s browsing context 0x%08" PRIx64, + GetUserActivationStateAndModifiers(), + 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 | + nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADED_FIRST)) == + aStateFlags, + "Invalid flags specified!"); + + if (XRE_IsParentProcess()) { + Canonical()->AddSecurityState(aStateFlags); + } else { + ContentChild* child = ContentChild::GetSingleton(); + child->SendAddSecurityState(this, aStateFlags); + } +} + +void WindowContext::NotifyUserGestureActivation( + UserActivation::Modifiers + aModifiers /* = UserActivation::Modifiers::None() */) { + UserActivation::StateAndModifiers stateAndModifiers; + stateAndModifiers.SetState(UserActivation::State::FullActivated); + stateAndModifiers.SetModifiers(aModifiers); + Unused << SetUserActivationStateAndModifiers(stateAndModifiers.GetRawData()); +} + +void WindowContext::NotifyResetUserGestureActivation() { + UserActivation::StateAndModifiers stateAndModifiers; + stateAndModifiers.SetState(UserActivation::State::None); + Unused << SetUserActivationStateAndModifiers(stateAndModifiers.GetRawData()); +} + +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) { + auto stateAndModifiers = UserActivation::StateAndModifiers( + GetUserActivationStateAndModifiers()); + stateAndModifiers.SetState(UserActivation::State::HasBeenActivated); + Unused << windowContext->SetUserActivationStateAndModifiers( + stateAndModifiers.GetRawData()); + } + }); + + return true; +} + +bool WindowContext::GetTransientUserGestureActivationModifiers( + UserActivation::Modifiers* aModifiers) { + if (!HasValidTransientUserGestureActivation()) { + return false; + } + + auto stateAndModifiers = + UserActivation::StateAndModifiers(GetUserActivationStateAndModifiers()); + *aModifiers = stateAndModifiers.GetModifiers(); + 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..fda4030045 --- /dev/null +++ b/docshell/base/WindowContext.h @@ -0,0 +1,429 @@ +/* -*- 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) \ + /* Whether this window is using its unpartitioned cookies due to \ + * the Storage Access API */ \ + FIELD(UsingStorageAccess, bool) \ + FIELD(ShouldResistFingerprinting, bool) \ + FIELD(OverriddenFingerprintingSettings, Maybe<RFPTarget>) \ + 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(UserActivationStateAndModifiers, \ + UserActivation::StateAndModifiers::DataT) \ + 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(); + } + + Nullable<uint64_t> GetOverriddenFingerprintingSettingsWebIDL() const { + Maybe<RFPTarget> overriddenFingerprintingSettings = + GetOverriddenFingerprintingSettings(); + + return overriddenFingerprintingSettings.isSome() + ? Nullable<uint64_t>( + uint64_t(overriddenFingerprintingSettings.ref())) + : Nullable<uint64_t>(); + } + + 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); + + UserActivation::State GetUserActivationState() const { + return UserActivation::StateAndModifiers( + GetUserActivationStateAndModifiers()) + .GetState(); + } + + // This function would be called when its corresponding window is activated + // by user gesture. + void NotifyUserGestureActivation( + UserActivation::Modifiers aModifiers = UserActivation::Modifiers::None()); + + // 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 GetTransientUserGestureActivationModifiers( + UserActivation::Modifiers* aModifiers); + + 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_UsingStorageAccess>, + const bool& aUsingStorageAccess, ContentParent* aSource); + bool CanSet(FieldIndex<IDX_ShouldResistFingerprinting>, + const bool& aShouldResistFingerprinting, ContentParent* aSource); + bool CanSet(FieldIndex<IDX_OverriddenFingerprintingSettings>, + const Maybe<RFPTarget>& aValue, 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_UserActivationStateAndModifiers>, + const UserActivation::StateAndModifiers::DataT& + aUserActivationStateAndModifiers, + 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_UserActivationStateAndModifiers>); + + // 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..8196e43016 --- /dev/null +++ b/docshell/base/crashtests/914521.html @@ -0,0 +1,38 @@ +<!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"; + setTimeout(finish,0); +} + +var win; +function finish() { + win.close(); + document.documentElement.removeAttribute("class"); +} + +function start() +{ + 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/metrics.yaml b/docshell/base/metrics.yaml new file mode 100644 index 0000000000..ddb3457945 --- /dev/null +++ b/docshell/base/metrics.yaml @@ -0,0 +1,29 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Core :: DOM: Navigation' + +performance.page: + total_content_page_load: + type: timing_distribution + time_unit: millisecond + telemetry_mirror: TOTAL_CONTENT_PAGE_LOAD_TIME + description: > + Time to load all of a page's resources and render. + (Migrated from the geckoview metric of the same name.) + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877842 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10 + notification_emails: + - perf-telemetry-alerts@mozilla.com + - bdekoz@mozilla.com + expires: never diff --git a/docshell/base/moz.build b/docshell/base/moz.build new file mode 100644 index 0000000000..3520e9d75a --- /dev/null +++ b/docshell/base/moz.build @@ -0,0 +1,126 @@ +# -*- 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") + +XPIDL_SOURCES += [ + "nsIDocShell.idl", + "nsIDocShellTreeItem.idl", + "nsIDocShellTreeOwner.idl", + "nsIDocumentLoaderFactory.idl", + "nsIDocumentViewer.idl", + "nsIDocumentViewerEdit.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..fdae228b90 --- /dev/null +++ b/docshell/base/nsAboutRedirector.cpp @@ -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/. */ + +#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/dom/ContentParent.h" +#include "mozilla/dom/RemoteType.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"); + } + + if (spec.EqualsASCII("about:crashextensions") && XRE_IsParentProcess()) { + using ContentParent = mozilla::dom::ContentParent; + nsTArray<RefPtr<ContentParent>> toKill; + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + if (cp->GetRemoteType() == EXTENSION_REMOTE_TYPE) { + toKill.AppendElement(cp); + } + } + for (auto& cp : toKill) { + cp->KillHard("Killed via about:crashextensions"); + } + } + + 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}, +#if !defined(ANDROID) && !defined(XP_WIN) + {"webauthn", "chrome://global/content/aboutWebauthn.html", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, +#endif + {"neterror", "chrome://global/content/aboutNetError.html", + 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", "about:processes", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI | + nsIAboutModule::HIDE_FROM_ABOUTABOUT}, + {"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::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS | + 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_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_CAN_LOAD_IN_CHILD | + nsIAboutModule::URI_MUST_LOAD_IN_CHILD}, + {"crashgpu", "about:blank", nsIAboutModule::HIDE_FROM_ABOUTABOUT}, + {"crashextensions", "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") || path.EqualsASCII("crashextensions")) { + bool isExternal; + aLoadInfo->GetLoadTriggeredFromExternal(&isExternal); + if (isExternal || !aLoadInfo->TriggeringPrincipal() || + !aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + 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..2eccb74da9 --- /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 "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->CreateDocumentViewer(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..f2a9e0fa59 --- /dev/null +++ b/docshell/base/nsDocShell.cpp @@ -0,0 +1,13763 @@ +/* -*- 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/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/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 "nsIController.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIDocumentViewer.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 "nsGlobalWindowInner.h" +#include "nsGlobalWindowOuter.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 "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), + 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() { + // Avoid notifying observers while we're in the dtor. + mIsBeingDestroyed = true; + + Destroy(); + + if (mDocumentViewer) { + mDocumentViewer->Close(nullptr); + mDocumentViewer->Destroy(); + mDocumentViewer = 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(EnsureDocumentViewer())) { + RefPtr<Document> doc = mDocumentViewer->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! + if (aLoadState->IsFormSubmission()) { +#ifdef DEBUG + if (!mEODForCurrentDocument) { + const MaybeDiscarded<BrowsingContext>& targetBC = + aLoadState->TargetBrowsingContext(); + MOZ_ASSERT_IF(GetBrowsingContext() == targetBC.get(), + aLoadState->LoadType() == LOAD_NORMAL_REPLACE); + } +#endif + } else { + 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 (mDocumentViewer && !mFiredUnloadEvent) { + // Keep an explicit reference since calling PageHide could release + // mDocumentViewer + nsCOMPtr<nsIDocumentViewer> viewer(mDocumentViewer); + mFiredUnloadEvent = true; + + if (mTiming) { + mTiming->NotifyUnloadEventStart(); + } + + viewer->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; + } + + if (RefPtr<nsGlobalWindowInner> inner = + nsGlobalWindowInner::Cast(mScriptGlobal->GetCurrentInnerWindow())) { + if (aThaw) { + inner->Thaw(false); + } else { + inner->Freeze(false); + } + } +} + +void nsDocShell::FirePageHideShowNonRecursive(bool aShow) { + MOZ_ASSERT(mozilla::BFCacheInParent()); + + if (!mDocumentViewer) { + 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<nsIDocumentViewer> viewer(mDocumentViewer); + if (aShow) { + viewer->SetIsHidden(false); + mRefreshURIList = std::move(mBFCachedRefreshURIList); + RefreshURIFromQueue(); + mFiredUnloadEvent = false; + RefPtr<Document> doc = viewer->GetDocument(); + if (doc) { + doc->NotifyActivityChanged(); + nsCOMPtr<nsPIDOMWindowInner> inner = + mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : 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; + viewer->PageHide(false); + + RefPtr<PresShell> presShell = GetPresShell(); + if (presShell) { + presShell->Freeze(false); + } + } +} + +nsresult nsDocShell::Dispatch(already_AddRefed<nsIRunnable>&& aRunnable) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + if (NS_WARN_IF(!GetWindow())) { + // Window should only be unavailable after destroyed. + MOZ_ASSERT(mIsBeingDestroyed); + return NS_ERROR_FAILURE; + } + return SchedulerGroup::Dispatch(runnable.forget()); +} + +NS_IMETHODIMP +nsDocShell::DispatchLocationChangeEvent() { + return Dispatch(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() { + nsIDocumentViewer* viewer = mDocumentViewer; + while (viewer) { + nsIDocumentViewer* prevViewer = viewer->GetPreviousViewer(); + if (!prevViewer) { + return viewer->GetPresContext(); + } + viewer = prevViewer; + } + + return nullptr; +} + +nsPresContext* nsDocShell::GetPresContext() { + if (!mDocumentViewer) { + return nullptr; + } + + return mDocumentViewer->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::GetDocViewer(nsIDocumentViewer** aDocumentViewer) { + NS_ENSURE_ARG_POINTER(aDocumentViewer); + + *aDocumentViewer = mDocumentViewer; + NS_IF_ADDREF(*aDocumentViewer); + 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; + } + + SetCurrentURIInternal(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; +} + +void nsDocShell::SetCurrentURIInternal(nsIURI* aURI) { + mCurrentURI = aURI; + if (mBrowsingContext) { + mBrowsingContext->ClearCachedValuesOfLocations(); + } +} + +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<nsIDocumentViewer> viewer; + GetDocViewer(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::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 (!mDocumentViewer) { + return NS_OK; + } + Document* doc = mDocumentViewer->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; +} + +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; +} + +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 (mDocumentViewer) { + Document* doc = mDocumentViewer->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() { + nsCOMPtr<nsPIDOMWindowInner> win = + mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : nullptr; + if (win) { + Navigator* navigator = win->Navigator(); + if (navigator) { + navigator->ClearPlatformCache(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::ClearCachedUserAgent() { + nsCOMPtr<nsPIDOMWindowInner> win = + mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : 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->GetCurrentInnerWindow() && + mScriptGlobal->GetCurrentInnerWindow()->GetExtantDoc()) { + MOZ_DIAGNOSTIC_ASSERT( + mScriptGlobal->GetCurrentInnerWindow()->GetClientInfo().isSome()); + MOZ_DIAGNOSTIC_ASSERT(!mInitialClientSource); + return; + } + + // Don't recreate the initial client source. We call this multiple times + // when DoChannelLoad() is called before CreateAboutBlankDocumentViewer. + 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 + // CreateAboutBlankDocumentViewer. 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, GetMainThreadSerialEventTarget(), 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; + } + + nsPIDOMWindowInner* innerWindow = + mScriptGlobal ? mScriptGlobal->GetCurrentInnerWindow() : 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::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 (!mDocumentViewer) { + return NS_OK; + } + Document* doc = mDocumentViewer->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(EnsureDocumentViewer(), nullptr); + return mDocumentViewer->GetDocument(); +} + +Document* nsDocShell::GetExtantDocument() { + return mDocumentViewer ? mDocumentViewer->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 (!mDocumentViewer) { + return true; + } + bool firingBeforeUnload; + mDocumentViewer->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 && + !mBrowsingContext->GetParentWindowContext()->IsInProcess()) { + 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) { + // 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"); + } + + errorPage.Assign("certerror"); + } 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->SetTriggeringWindowId( + mBrowsingContext->GetCurrentInnerWindowId()); + nsPIDOMWindowInner* innerWin = mScriptGlobal->GetCurrentInnerWindow(); + if (innerWin) { + loadState->SetTriggeringStorageAccess(innerWin->UsingStorageAccess()); + } + } + 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<nsIDocumentViewer> viewer(mDocumentViewer); + NS_ENSURE_STATE(viewer); + + bool okToUnload = true; + MOZ_TRY(viewer->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(); + uint64_t triggeringWindowId = aDocument->InnerWindowID(); + bool triggeringStorageAccess = aDocument->UsingStorageAccess(); + + 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->SetTriggeringWindowId(triggeringWindowId); + loadState->SetTriggeringStorageAccess(triggeringStorageAccess); + 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 (mDocumentViewer) { + nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer; + viewer->Stop(); + } + } else if (nsIWebNavigation::STOP_NETWORK & aStopFlags) { + // Stop the document loading only + if (mDocumentViewer) { + RefPtr<Document> doc = mDocumentViewer->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 CreateDocumentViewer. + 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(EnsureDocumentViewer(), NS_ERROR_FAILURE); + + RefPtr<Document> doc = mDocumentViewer->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 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 (mDocumentViewer) { + mDocumentViewer->Close(nullptr); + mDocumentViewer->Destroy(); + mDocumentViewer = nullptr; + } + + nsDocLoader::Destroy(); + + mParentWidget = nullptr; + SetCurrentURIInternal(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()->EvictLocalDocumentViewers(); + } + + 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 (mDocumentViewer) { + NS_ENSURE_SUCCESS(mDocumentViewer->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 mDocumentViewer + nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer; + if (viewer) { + uint32_t cvflags = (aFlags & nsIBaseWindow::eDelayResize) + ? nsIDocumentViewer::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 (!mDocumentViewer) { + 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 && mDocumentViewer) { + if (Document* doc = mDocumentViewer->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 mDocumentViewer. + nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer; + if (!viewer) { + return NS_OK; + } + if (aVisibility) { + viewer->Show(); + } else { + viewer->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->SetTriggeringWindowId(doc->InnerWindowID()); + loadState->SetTriggeringStorageAccess(doc->UsingStorageAccess()); + } + + 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(nsIDocumentViewer* aDocumentViewer, + 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(aDocumentViewer, 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) { + 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; + } + + 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, + bool* outWasSchemelessInput) { + 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)); + info->GetWasSchemelessInput(outWasSchemelessInput); + 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_FLAG_NONE, + 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()) { + glean::performance_page::total_content_page_load.AccumulateRawDuration( + TimeStamp::Now() - 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 DocumentViewer that the Document has finished loading. This + // will cause any OnLoad(...) and PopState(...) handlers to fire. + if (!mEODForCurrentDocument && mDocumentViewer) { + mIsExecutingOnLoadHandler = true; + nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer; + viewer->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::EnsureDocumentViewer() { + if (mDocumentViewer) { + 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 = CreateAboutBlankDocumentViewer( + principal, partitionedPrincipal, cspToInheritForAboutBlank, baseURI, + /* aIsInitialDocument */ true); + + NS_ENSURE_STATE(mDocumentViewer); + + if (NS_SUCCEEDED(rv)) { + RefPtr<Document> doc(GetDocument()); + MOZ_ASSERT(doc, + "Should have doc if CreateAboutBlankDocumentViewer " + "succeeded!"); + MOZ_ASSERT(doc->IsInitialDocument(), "Document should be initial document"); + + // Documents created using EnsureDocumentViewer 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::CreateAboutBlankDocumentViewer( + 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<nsIDocumentViewer> 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; + } + + // mDocumentViewer->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 (mDocumentViewer) { + 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 = mDocumentViewer->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::FindInternalDocumentViewer("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::CreateAboutBlankDocumentViewer(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal, + nsIContentSecurityPolicy* aCSP) { + return CreateAboutBlankDocumentViewer(aPrincipal, aPartitionedPrincipal, aCSP, + nullptr, + /* aIsInitialDocument */ false); +} + +nsresult nsDocShell::CreateDocumentViewerForActor( + WindowGlobalChild* aWindowActor) { + MOZ_ASSERT(aWindowActor); + + // FIXME: WindowGlobalChild should provide the PartitionedPrincipal. + // FIXME: We may want to support non-initial documents here. + nsresult rv = CreateAboutBlankDocumentViewer( + 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 CreateAboutBlankDocumentViewer 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<nsIDocumentViewer> viewer = mOSHE->GetDocumentViewer(); + 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 (mDocumentViewer) { + nsIntRect bounds; + mDocumentViewer->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(nsIDocumentViewer* aDocumentViewer, bool aTop) { + MOZ_ASSERT(!mozilla::SessionHistoryInParent()); + + nsresult rv; + if (!aDocumentViewer) { + rv = EnsureDocumentViewer(); + NS_ENSURE_SUCCESS(rv, rv); + + aDocumentViewer = mDocumentViewer; + } + + // 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 = aDocumentViewer->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 CreateDocumentViewer 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<nsIDocumentViewer> viewer = aSHEntry->GetDocumentViewer(); + + 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->SetDocumentViewer(nullptr); + return NS_ERROR_FAILURE; + } + + NS_ASSERTION(mDocumentViewer != 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(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 CreateDocumentViewer. + if (!mLSHE) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDocumentViewer> viewer = mLSHE->GetDocumentViewer(); + 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 (mDocumentViewer) { + 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 (mDocumentViewer) { + // Make sure to hold a strong ref to previousViewer here while we + // drop the reference to it from mDocumentViewer. + nsCOMPtr<nsIDocumentViewer> previousViewer = + mDocumentViewer->GetPreviousViewer(); + if (previousViewer) { + mDocumentViewer->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(); + + mDocumentViewer->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 mDocumentViewer. 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 (mDocumentViewer) { + mDocumentViewer->Close(mSavingOldViewer ? mOSHE.get() : nullptr); + viewer->SetPreviousViewer(mDocumentViewer); + } + if (mOSHE && (!mDocumentViewer || !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 mDocumentViewer setup just like Embed does. + mDocumentViewer = 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(); + + mDocumentViewer.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 = mDocumentViewer->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 = mDocumentViewer->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->SetDocumentViewer(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. + mDocumentViewer->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 CreateDocumentViewer() 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 mDocumentViewer->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)); + mDocumentViewer->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::CreateDocumentViewer(const nsACString& aContentType, + nsIRequest* aRequest, + nsIStreamListener** aContentHandler) { + *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<nsIDocumentViewer> viewer; + nsresult rv = NewDocumentViewerObj(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); + } + + // 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, 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); + } + + NS_ENSURE_SUCCESS(Embed(viewer, nullptr, false, + ShouldAddToSessionHistory(finalURI, aOpenedChannel), + aOpenedChannel, previousURI), + NS_ERROR_FAILURE); + + if (!mBrowsingContext->GetHasLoadedNonInitialDocument()) { + MOZ_ALWAYS_SUCCEEDS(mBrowsingContext->SetHasLoadedNonInitialDocument(true)); + } + + 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::NewDocumentViewerObj(const nsACString& aContentType, + nsIRequest* aRequest, + nsILoadGroup* aLoadGroup, + nsIStreamListener** aContentHandler, + nsIDocumentViewer** aViewer) { + nsCOMPtr<nsIChannel> aOpenedChannel = do_QueryInterface(aRequest); + + nsCOMPtr<nsIDocumentLoaderFactory> docLoaderFactory = + nsContentUtils::FindInternalDocumentViewer(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(nsIDocumentViewer* 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<nsIDocumentViewer> newViewer; + + if (mDocumentViewer || parent) { + nsCOMPtr<nsIDocumentViewer> oldViewer; + if (mDocumentViewer) { + // 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 + oldViewer = mDocumentViewer; + + // 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->GetDocViewer(getter_AddRefs(oldViewer)); + } + + if (oldViewer) { + newViewer = aNewViewer; + if (newViewer) { + reloadEncoding = + oldViewer->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<nsIDocumentViewer> viewer = mDocumentViewer; + if (viewer) { + // Stop any activity that may be happening in the old document before + // releasing it... + viewer->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 = viewer->GetPresShell()) { + bgcolor = presShell->GetCanvasBackground(); + isUnderHiddenEmbedderElement = presShell->IsUnderHiddenEmbedderElement(); + } + + viewer->Close(mSavingOldViewer ? mOSHE.get() : nullptr); + aNewViewer->SetPreviousViewer(viewer); + } + if (mOSHE && (!mDocumentViewer || !mSavingOldViewer)) { + // We don't plan to save a viewer in mOSHE; tell it to drop + // any other state it's holding. + mOSHE->SyncPresentationState(); + } + + mDocumentViewer = 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(); + + mDocumentViewer = aNewViewer; + + nsCOMPtr<nsIWidget> widget; + NS_ENSURE_SUCCESS(GetMainWidget(getter_AddRefs(widget)), NS_ERROR_FAILURE); + + nsIntRect bounds(x, y, cx, cy); + + mDocumentViewer->SetNavigationTiming(mTiming); + + if (NS_FAILED(mDocumentViewer->Init(widget, bounds, aWindowActor))) { + nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer; + viewer->Close(nullptr); + viewer->Destroy(); + mDocumentViewer = nullptr; + SetCurrentURIInternal(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 (newViewer) { + newViewer->SetReloadEncodingAndSource(reloadEncoding, reloadEncodingSource); + } + + NS_ENSURE_TRUE(mDocumentViewer, 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 = mDocumentViewer->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 mDocumentViewer 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(mDocumentViewer); + + 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, + &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(mDocumentViewer, NS_ERROR_FAILURE); + Document* doc = mDocumentViewer->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->SetTriggeringWindowId(aLoadState->TriggeringWindowId()); + loadState->SetTriggeringStorageAccess( + aLoadState->TriggeringStorageAccess()); + 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(aLoadState->LoadType()); + 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); + if (aLoadState->IsFormSubmission()) { + aLoadState->SetLoadType( + GetLoadTypeForFormSubmission(targetContext, aLoadState)); + } + + // + // 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, + bool& aSameDocument) { + aSameDocument = true; +#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())); + } + + if (StaticPrefs::dom_security_setdocumenturi()) { + // check if aLoadState->URI(), principalURI, mCurrentURI are same origin + // skip handling otherwise + nsCOMPtr<nsIPrincipal> origPrincipal = doc->NodePrincipal(); + nsCOMPtr<nsIURI> principalURI = origPrincipal->GetURI(); + if (origPrincipal->GetIsNullPrincipal()) { + nsCOMPtr<nsIPrincipal> precursor = origPrincipal->GetPrecursorPrincipal(); + if (precursor) { + principalURI = precursor->GetURI(); + } + } + + auto isLoadableViaInternet = [](nsIURI* uri) { + return (uri && (net::SchemeIsHTTP(uri) || net::SchemeIsHTTPS(uri))); + }; + + if (isLoadableViaInternet(principalURI) && + isLoadableViaInternet(mCurrentURI) && isLoadableViaInternet(newURI)) { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (!NS_SUCCEEDED( + ssm->CheckSameOriginURI(newURI, principalURI, false, false)) || + !NS_SUCCEEDED(ssm->CheckSameOriginURI(mCurrentURI, principalURI, + false, false))) { + MOZ_LOG(gSHLog, LogLevel::Debug, + ("nsDocShell[%p]: possible violation of the same origin policy " + "during same document navigation", + this)); + aSameDocument = false; + return NS_OK; + } + } + } + +#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, true, true); + + nsCOMPtr<nsIInputStream> postData; + nsCOMPtr<nsIReferrerInfo> referrerInfo; + 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, page ident and referrer info 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(); + referrerInfo = mOSHE->GetReferrerInfo(); + } + + // 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); + } + + // As the document has not changed, the referrer info hasn't changed too, + // so we can just copy it over. + if (referrerInfo) { + mOSHE->SetReferrerInfo(referrerInfo); + } + } + + /* 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, page ident and referrer info 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(); + referrerInfo = mActiveEntry->GetReferrerInfo(); + } + } + + 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); + } + + // As the document has not changed, the referrer info hasn't changed too, + // so we can just copy it over. + if (referrerInfo) { + mActiveEntry->SetReferrerInfo(referrerInfo); + } + + // 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; + nsCOMPtr<nsPIDOMWindowInner> win = + scriptGlobal ? scriptGlobal->GetCurrentInnerWindow() : 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); +} + +uint32_t nsDocShell::GetLoadTypeForFormSubmission( + BrowsingContext* aTargetBC, nsDocShellLoadState* aLoadState) { + MOZ_ASSERT(aLoadState->IsFormSubmission()); + + // https://html.spec.whatwg.org/#form-submission-algorithm + // 22. Let historyHandling be "push". + // 23. If form document equals targetNavigable's active document, and + // form document has not yet completely loaded, then set + // historyHandling to "replace". + return GetBrowsingContext() == aTargetBC && !mEODForCurrentDocument + ? LOAD_NORMAL_REPLACE + : LOAD_LINK; +} + +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); + } + + // This is the non-retargeting load path, we've already set the right loadtype + // for form submissions in nsDocShell::OnLinkClickSync. + 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(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 = CreateAboutBlankDocumentViewer(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, sameDocument); + NS_ENSURE_SUCCESS(rv, rv); + if (shouldTakeFocus) { + mBrowsingContext->Focus(CallerType::System, IgnoreErrors()); + } + if (sameDocument) { + return rv; + } + } + + // mDocumentViewer->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() && mDocumentViewer) { + 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->IsExemptFromHTTPSFirstMode() && + nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) { + rv = mDocumentViewer->PermitUnload( + nsIDocumentViewer::PermitUnloadAction::eDontPromptAndUnload, + &okToUnload); + } else { + rv = mDocumentViewer->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 ((mDocumentViewer && mDocumentViewer->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 mDocumentViewer 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 (mDocumentViewer) { + nsCOMPtr<nsIDocumentViewer> prevViewer = + mDocumentViewer->GetPreviousViewer(); + if (prevViewer) { +#ifdef DEBUG + nsCOMPtr<nsIDocumentViewer> 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 + mDocumentViewer->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 && mDocumentViewer) { + document = mDocumentViewer->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. + EnsureDocumentViewer(); + if (!mDocumentViewer) { + return nullptr; + } + document = mDocumentViewer->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)) && + (!mDocumentViewer || !mDocumentViewer->GetDocument() || + mDocumentViewer->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()) { + if (context->HasValidTransientUserGestureActivation()) { + aLoadState->SetHasValidUserGestureActivation(true); + } + if (!aLoadState->TriggeringWindowId()) { + aLoadState->SetTriggeringWindowId(context->Id()); + } + if (!aLoadState->TriggeringStorageAccess()) { + Document* contextDoc = context->GetExtantDoc(); + if (contextDoc) { + aLoadState->SetTriggeringStorageAccess( + contextDoc->UsingStorageAccess()); + } + } + } + + // 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->SetTriggeringWindowId(aLoadState->TriggeringWindowId()); + loadInfo->SetTriggeringStorageAccess(aLoadState->TriggeringStorageAccess()); + 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 isEmbeddingBlockedError = false; + if (mFailedChannel) { + nsresult status; + mFailedChannel->GetStatus(&status); + isEmbeddingBlockedError = status == NS_ERROR_XFO_VIOLATION || + status == NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION; + } + + nsLoadFlags loadFlags = aLoadState->CalculateChannelLoadFlags( + mBrowsingContext, Some(uriModified), Some(isEmbeddingBlockedError)); + + nsCOMPtr<nsIChannel> channel; + if (DocumentChannel::CanUseDocumentChannel(aLoadState->URI()) && + !isAboutBlankLoadOntoInitialAboutBlank) { + channel = DocumentChannel::CreateForDocument( + aLoadState, loadInfo, loadFlags, this, cacheKey, uriModified, + isEmbeddingBlockedError); + 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); + } + + 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, + GetMainThreadSerialEventTarget()); + docChannel->SetInitialClientInfo(GetInitialClientInfo()); + } else { + rv = AddClientChannelHelper(aChannel, std::move(noReservedClient), + GetInitialClientInfo(), + GetMainThreadSerialEventTarget()); + } + 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()); + } + + // 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, GetMainThreadSerialEventTarget()); + + 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; +} + +// https://html.spec.whatwg.org/#scrolling-to-a-fragment +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 it's a load from history, we don't have any anchor jumping to do. + // Scrollbar position will be restored by the caller based on positions stored + // in session history. + bool scroll = aLoadType != LOAD_HISTORY && aLoadType != LOAD_RELOAD_NORMAL; + + if (aNewHash.IsEmpty()) { + // 2. If fragment is the empty string, then return the special value top of + // the document. + // + // Tell the shell it's at an anchor without scrolling. + presShell->GoToAnchor(u""_ns, false); + + if (scroll) { + // 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; + } + + // 3. Let potentialIndicatedElement be the result of finding a potential + // indicated element given document and fragment. + NS_ConvertUTF8toUTF16 uStr(aNewHash); + auto rv = presShell->GoToAnchor(uStr, scroll, ScrollFlags::ScrollSmoothAuto); + + // 4. If potentialIndicatedElement is not null, then return + // potentialIndicatedElement. + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + + // 5. Let fragmentBytes be the result of percent-decoding fragment. + nsAutoCString fragmentBytes; + const bool unescaped = NS_UnescapeURL(aNewHash.Data(), aNewHash.Length(), + /* aFlags = */ 0, fragmentBytes); + + if (!unescaped) { + // Another attempt is only necessary if characters were unescaped. + return NS_OK; + } + + if (fragmentBytes.IsEmpty()) { + // When aNewHash contains "%00", the unescaped string may be empty, and + // GoToAnchor asserts if we ask it to scroll to an empty ref. + presShell->GoToAnchor(u""_ns, false); + return NS_OK; + } + + // 6. Let decodedFragment be the result of running UTF-8 decode without BOM on + // fragmentBytes. + nsAutoString decodedFragment; + rv = UTF_8_ENCODING->DecodeWithoutBOMHandling(fragmentBytes, decodedFragment); + NS_ENSURE_SUCCESS(rv, rv); + + // 7. Set potentialIndicatedElement to the result of finding a potential + // indicated element given document and decodedFragment. + // + // Ignore the 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). + presShell->GoToAnchor(decodedFragment, scroll, ScrollFlags::ScrollSmoothAuto); + + return NS_OK; +} + +bool nsDocShell::OnNewURI(nsIURI* aURI, nsIChannel* aChannel, + nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aPrincipalToInherit, + nsIPrincipal* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, + 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, false, + /* 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 = mDocumentViewer->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 EvictOutOfRangeDocumentViewers 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()->EvictDocumentViewersOrReplaceEntry(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 CreateAboutBlankDocumentViewer 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 = CreateAboutBlankDocumentViewer( + 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, bool aWasUpgraded) { + 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; + } + + if (aWasUpgraded) { + visitURIFlags |= + IHistory::REDIRECT_SOURCE | IHistory::REDIRECT_SOURCE_UPGRADED; + } + + 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, + false); +} + +//***************************************************************************** +// 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(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; + uint64_t triggeringWindowId = 0; + bool triggeringStorageAccess = false; + if (mBrowsingContext) { + triggeringSandboxFlags = aContent->OwnerDoc()->GetSandboxFlags(); + triggeringWindowId = aContent->OwnerDoc()->InnerWindowID(); + triggeringStorageAccess = aContent->OwnerDoc()->UsingStorageAccess(); + } + + 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(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); + } + + uint32_t loadType = LOAD_LINK; + if (aLoadState->IsFormSubmission()) { + if (aLoadState->Target().IsEmpty()) { + // We set the right load type here for form submissions with an empty + // target. Form submission with a non-empty target are handled in + // nsDocShell::PerformRetargeting after we've selected the correct target + // BC. + loadType = GetLoadTypeForFormSubmission(GetBrowsingContext(), aLoadState); + } + } else { + // Link click can be triggered inside an onload handler, and we don't want + // to add history entry in this case. + bool inOnLoadHandler = false; + GetIsExecutingOnLoadHandler(&inOnLoadHandler); + if (inOnLoadHandler) { + loadType = LOAD_NORMAL_REPLACE; + } + } + + nsCOMPtr<nsIReferrerInfo> referrerInfo = + elementCanHaveNoopener ? new ReferrerInfo(*aContent->AsElement()) + : new ReferrerInfo(*referrerDoc); + RefPtr<WindowContext> context = mBrowsingContext->GetCurrentWindowContext(); + + aLoadState->SetTriggeringSandboxFlags(triggeringSandboxFlags); + aLoadState->SetTriggeringWindowId(triggeringWindowId); + aLoadState->SetTriggeringStorageAccess(triggeringStorageAccess); + 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; +} + +//---------------------------------------------------------------------- +// 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<nsIDocumentViewer> viewer; + NS_ENSURE_SUCCESS(GetDocViewer(getter_AddRefs(viewer)), NS_ERROR_FAILURE); + if (viewer) { + int32_t source; + Unused << viewer->GetReloadEncodingAndSource(&source); + if (aSource > source) { + viewer->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(mDocumentViewer); + 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; } + +/* 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..9f2d9a17dc --- /dev/null +++ b/docshell/base/nsDocShell.h @@ -0,0 +1,1366 @@ +/* -*- 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/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 nsIController; +class nsIDocShellTreeOwner; +class nsIDocumentViewer; +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); + } + + 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 CreateDocumentViewerForActor( + 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, + bool* outWasSchemelessInput = 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 HasDocumentViewer() const { return !!mDocumentViewer; } + + 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; + + 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 EnsureDocumentViewer(); + + // 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 CreateAboutBlankDocumentViewer( + 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 CreateDocumentViewer(const nsACString& aContentType, + nsIRequest* aRequest, + nsIStreamListener** aContentHandler); + + nsresult NewDocumentViewerObj(const nsACString& aContentType, + nsIRequest* aRequest, nsILoadGroup* aLoadGroup, + nsIStreamListener** aContentHandler, + nsIDocumentViewer** aViewer); + + already_AddRefed<nsILoadURIDelegate> GetLoadURIDelegate(); + + nsresult SetupNewViewer( + nsIDocumentViewer* 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); + + // This returns the load type for a form submission (see + // https://html.spec.whatwg.org/#form-submission-algorithm). The load type + // should be set as soon as the target BC has been determined. + uint32_t GetLoadTypeForFormSubmission( + mozilla::dom::BrowsingContext* aTargetBC, + nsDocShellLoadState* aLoadState); + + private: + // Returns true if 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* aPartitionedPrincipalToInherit, + nsIContentSecurityPolicy* aCsp, 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, bool aWasUpgraded); + + 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. + + // mDocumentViewer 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 + // mDocumentViewer. + // 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 mDocumentViewer and removed from the history entry. + // (mDocumentViewer is put into session history as described above, if + // applicable). + + // Determines whether we can safely cache the current mDocumentViewer 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(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(nsIDocumentViewer* aDocumentViewer, + 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, + bool& aSameDocument); + + 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: + void SetCurrentURIInternal(nsIURI* aURI); + + // 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<nsIDocumentViewer> mDocumentViewer; + 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; + + // mCurrentURI should be marked immutable on set if possible. + // Change mCurrentURI only through SetCurrentURIInternal method. + 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 CreateDocumentViewer. 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; + + // 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 CreateDocumentViewer() 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..587617e73d --- /dev/null +++ b/docshell/base/nsDocShellLoadState.cpp @@ -0,0 +1,1325 @@ +/* -*- 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 "nsIProtocolHandler.h" +#include "nsISHEntry.h" +#include "nsIURIFixup.h" +#include "nsIWebNavigation.h" +#include "nsIChannel.h" +#include "nsIURLQueryStringStripper.h" +#include "nsIXULRuntime.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(); + mIsExemptFromHTTPSFirstMode = aLoadState.IsExemptFromHTTPSFirstMode(); + 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(); + mTriggeringWindowId = aLoadState.TriggeringWindowId(); + mTriggeringStorageAccess = aLoadState.TriggeringStorageAccess(); + mTriggeringRemoteType = aLoadState.TriggeringRemoteType(); + mWasSchemelessInput = aLoadState.WasSchemelessInput(); + 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), + mTriggeringWindowId(aOther.mTriggeringWindowId), + mTriggeringStorageAccess(aOther.mTriggeringStorageAccess), + 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), + mIsExemptFromHTTPSFirstMode(aOther.mIsExemptFromHTTPSFirstMode), + 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), + mWasSchemelessInput(aOther.mWasSchemelessInput) { + 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), + mTriggeringWindowId(0), + mTriggeringStorageAccess(false), + mKeepResultPrincipalURIIfSet(false), + mLoadReplace(false), + mInheritPrincipal(false), + mPrincipalIsExplicit(false), + mNotifiedBeforeUnloadListeners(false), + mForceAllowDataURI(false), + mIsExemptFromHTTPSFirstMode(false), + mOriginalFrameSrc(false), + mIsFormSubmission(false), + mLoadType(LOAD_NORMAL), + 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), + mWasSchemelessInput(false) { + 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->SetTriggeringWindowId(aLoadURIOptions.mTriggeringWindowId); + loadState->SetTriggeringStorageAccess( + aLoadURIOptions.mTriggeringStorageAccess); + 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->SetWasSchemelessInput(aLoadURIOptions.mWasSchemelessInput); + + 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; +} + +void nsDocShellLoadState::SetTriggeringWindowId(uint64_t aTriggeringWindowId) { + mTriggeringWindowId = aTriggeringWindowId; +} + +uint64_t nsDocShellLoadState::TriggeringWindowId() const { + return mTriggeringWindowId; +} + +void nsDocShellLoadState::SetTriggeringStorageAccess( + bool aTriggeringStorageAccess) { + mTriggeringStorageAccess = aTriggeringStorageAccess; +} + +bool nsDocShellLoadState::TriggeringStorageAccess() const { + return mTriggeringStorageAccess; +} + +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::IsExemptFromHTTPSFirstMode() const { + return mIsExemptFromHTTPSFirstMode; +} + +void nsDocShellLoadState::SetIsExemptFromHTTPSFirstMode( + bool aIsExemptFromHTTPSFirstMode) { + mIsExemptFromHTTPSFirstMode = aIsExemptFromHTTPSFirstMode; +} + +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; +} + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED +void nsDocShellLoadState::AssertProcessCouldTriggerLoadIfSystem() { + // Early check to see if we're trying to start a file URI load with a system + // principal within a web content process. + // If this assertion fails, the load will fail later during + // nsContentSecurityManager checks, however this assertion should happen + // closer to whichever caller is triggering the system-principal load. + if (mozilla::SessionHistoryInParent() && + TriggeringPrincipal()->IsSystemPrincipal() && + mozilla::dom::IsWebRemoteType(GetEffectiveTriggeringRemoteType())) { + bool localFile = false; + if (NS_SUCCEEDED(NS_URIChainHasFlags( + URI(), nsIProtocolHandler::URI_IS_LOCAL_FILE, &localFile)) && + localFile) { + NS_WARNING(nsPrintfCString("Unexpected system load of file URI (%s) from " + "web content process", + URI()->GetSpecOrDefault().get()) + .get()); + MOZ_CRASH("Unexpected system load of file URI from web content process"); + } + } +} +#endif + +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> aIsEmbeddingBlockedError) { + 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, aIsEmbeddingBlockedError.isSome()); + + if (loadType == LOAD_ERROR_PAGE) { + // Error pages are LOAD_BACKGROUND, unless it's an + // XFO / frame-ancestors error for which we want an error page to load + // but additionally want the onload() event to fire. + if (!*aIsEmbeddingBlockedError) { + 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.IsExemptFromHTTPSFirstMode() = mIsExemptFromHTTPSFirstMode; + 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.TriggeringWindowId() = mTriggeringWindowId; + loadState.TriggeringStorageAccess() = mTriggeringStorageAccess; + loadState.TriggeringRemoteType() = mTriggeringRemoteType; + loadState.WasSchemelessInput() = mWasSchemelessInput; + 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..a34ca1b54b --- /dev/null +++ b/docshell/base/nsDocShellLoadState.h @@ -0,0 +1,609 @@ +/* -*- 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); + + uint64_t TriggeringWindowId() const; + + void SetTriggeringWindowId(uint64_t aTriggeringWindowId); + + bool TriggeringStorageAccess() const; + + void SetTriggeringStorageAccess(bool aTriggeringStorageAccess); + + 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 IsExemptFromHTTPSFirstMode() const; + + void SetIsExemptFromHTTPSFirstMode(bool aIsExemptFromHTTPSFirstMode); + + 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); + } + + void SetWasSchemelessInput(bool aWasSchemelessInput) { + mWasSchemelessInput = aWasSchemelessInput; + } + + bool GetWasSchemelessInput() { return mWasSchemelessInput; } + + // 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); + + // Diagnostic assert if this is a system-principal triggered load, and it is + // trivial to determine that the effective triggering remote type would not be + // allowed to perform this load. + // + // This is called early during the load to crash as close to the cause as + // possible. See bug 1838686 for details. +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + void AssertProcessCouldTriggerLoadIfSystem(); +#else + void AssertProcessCouldTriggerLoadIfSystem() {} +#endif + + // 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 + // aIsEmbeddingBlockedError are expected to be Nothing when called from parent + // process. + nsLoadFlags CalculateChannelLoadFlags( + mozilla::dom::BrowsingContext* aBrowsingContext, + mozilla::Maybe<bool> aUriModified, + mozilla::Maybe<bool> aIsEmbeddingBlockedError); + + 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 window ID and current "has storage access" value of the entity + // triggering the load. This allows the identification of self-initiated + // same-origin navigations that should propogate unpartitioned storage access. + uint64_t mTriggeringWindowId; + bool mTriggeringStorageAccess; + + // 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 mIsExemptFromHTTPSFirstMode; + + // 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. + 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; + + // if the to-be-loaded address had it protocol added through a fixup + bool mWasSchemelessInput = false; +}; + +#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..bd4ed865bd --- /dev/null +++ b/docshell/base/nsDocShellTelemetryUtils.cpp @@ -0,0 +1,202 @@ +/* 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_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..9f1ab23a6c --- /dev/null +++ b/docshell/base/nsDocShellTreeOwner.cpp @@ -0,0 +1,1337 @@ +/* -*- 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/Try.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) { + if (nsCOMPtr<EventTarget> eventTarget = aMouseEvent->GetComposedTarget()) { + mPossibleTooltipNode = nsINode::FromEventTarget(eventTarget); + } + + if (mPossibleTooltipNode) { + nsresult rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(mTooltipTimer), sTooltipCallback, this, + LookAndFeel::GetInt(LookAndFeel::IntID::TooltipDelay, 500), + nsITimer::TYPE_ONE_SHOT, "ChromeTooltipListener::MouseMove", + GetMainThreadSerialEventTarget()); + 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/nsIDocShell.idl b/docshell/base/nsIDocShell.idl new file mode 100644 index 0000000000..21f09a517e --- /dev/null +++ b/docshell/base/nsIDocShell.idl @@ -0,0 +1,766 @@ +/* -*- 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 nsIContentSecurityPolicy; +interface nsIDocumentViewer; +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; + +/** + * nsIDocShell is an interface corresponding to the native nsDocShell object, + * which is a legacy in-process object roughly corresponding to a 'browsing + * context', as created for a browser tab or an iframe, for example. + * + * nsIDocShell has a 1:1 relationship with its paired dom::BrowsingContext and + * nsGlobalWindowOuter. It may be replaced during navigation. + * + * See also the comment documenting dom::BrowsingContext and the documentation + * at: + * + * https://html.spec.whatwg.org/multipage/document-sequences.html#browsing-context + * https://firefox-source-docs.mozilla.org/dom/navigation/embedding.html + * https://firefox-source-docs.mozilla.org/dom/navigation/nav_replace.html + */ +[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; + + /** + * Document Viewer that is currently loaded for this DocShell. This may + * change as the underlying content changes. + */ + [infallible] readonly attribute nsIDocumentViewer docViewer; + + /** + * 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; + + /** + * 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 nsIDocumentViewer 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 createAboutBlankDocumentViewer(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); + + /** + * 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(); + + /** + * 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; + + /** + * True iff asynchronous panning and zooming is enabled for this + * docshell. + */ + readonly attribute bool asyncPanZoomEnabled; + + /** + * 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(); + + /** + * 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..e3df2b2241 --- /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 nsIDocumentViewer; +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 { + nsIDocumentViewer createInstance(in string aCommand, + in nsIChannel aChannel, + in nsILoadGroup aLoadGroup, + in ACString aContentType, + in nsIDocShell aContainer, + in nsISupports aExtraInfo, + out nsIStreamListener aDocListenerResult); + + nsIDocumentViewer createInstanceForDocument(in nsISupports aContainer, + in Document aDocument, + in string aCommand); +}; diff --git a/docshell/base/nsIDocumentViewer.idl b/docshell/base/nsIDocumentViewer.idl new file mode 100644 index 0000000000..afbbdfc464 --- /dev/null +++ b/docshell/base/nsIDocumentViewer.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(48118355-e9a5-4452-ab18-59cc426fb817)] +interface nsIDocumentViewer : 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 nsIDocumentViewer_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] nsIDocumentViewer_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 nsIDocumentViewer 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 = nsIDocumentViewer::PermitUnloadAction; +using PermitUnloadResult = nsIDocumentViewer::PermitUnloadResult; + +} // namespace dom +} // namespace mozilla +%} diff --git a/docshell/base/nsIDocumentViewerEdit.idl b/docshell/base/nsIDocumentViewerEdit.idl new file mode 100644 index 0000000000..10ec203df7 --- /dev/null +++ b/docshell/base/nsIDocumentViewerEdit.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(e39a0c2a-5b31-4d57-a971-66ba07fab614)] +interface nsIDocumentViewerEdit : 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/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..2261c0cb40 --- /dev/null +++ b/docshell/base/nsIURIFixup.idl @@ -0,0 +1,204 @@ +/* -*- 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 there was no protocol at all and we had to add one in the first place. + */ + attribute boolean wasSchemelessInput; + + /** + * 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; + + /** + * 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..094074a0b9 --- /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(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", + GetMainThreadSerialEventTarget()); +} + +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..cb989a33f1 --- /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::DocumentViewerType vtype = nsContentUtils::TYPE_UNSUPPORTED; + + nsCOMPtr<nsIDocumentLoaderFactory> docLoaderFactory = + nsContentUtils::FindInternalDocumentViewer(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/build/components.conf b/docshell/build/components.conf new file mode 100644 index 0000000000..691277a0bb --- /dev/null +++ b/docshell/build/components.conf @@ -0,0 +1,194 @@ +# -*- 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', + 'crashextensions', + 'credits', + 'httpsonlyerror', + 'license', + 'logging', + 'logo', + 'memory', + 'mozilla', + 'neterror', + 'networking', + 'performance', + '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') +if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android' and buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'windows': + about_pages.append('webauthn') + +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..5a14b9dff8 --- /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.toml", + "test/unit_ipc/xpcshell.toml", +] + +MOCHITEST_MANIFESTS += [ + "test/iframesandbox/mochitest.toml", + "test/mochitest/mochitest.toml", + "test/navigation/mochitest.toml", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "test/chrome/chrome.toml", +] + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.toml", + "test/navigation/browser.toml", +] + +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..9148491718 --- /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::EvictLocalDocumentViewers() { + if (!mozilla::SessionHistoryInParent()) { + mHistory->EvictAllDocumentViewers(); + } +} + +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..7187240471 --- /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 EvictLocalDocumentViewers(); + + // 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..692ab4fe44 --- /dev/null +++ b/docshell/shistory/SessionHistoryEntry.cpp @@ -0,0 +1,1831 @@ +/* -*- 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. On a reload we still want to check, + // because the exemptions set by the user could have changed. + if ((mLoadType & nsIDocShell::LOAD_CMD_RELOAD) == 0) { + aLoadState.SetIsExemptFromHTTPSFirstMode(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::GetDocumentViewer(nsIDocumentViewer** aDocumentViewer) { + *aDocumentViewer = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +SessionHistoryEntry::SetDocumentViewer(nsIDocumentViewer* aDocumentViewer) { + 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->EvictOutOfRangeDocumentViewers(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..c7f1fea5e9 --- /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; + } + + nsCOMPtr<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..1d7427f581 --- /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 nsIDocumentViewer; +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; + + /** Document viewer, for fast restoration of presentation */ + [infallible] attribute nsIDocumentViewer documentViewer; + + [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 documentViewer. 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 |documentViewer| or |windowState| are null, then all of the + * following members are cleared/reset: + * documentViewer, 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..9ca237c670 --- /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 evictOutOfRangeDocumentViewers(in long aIndex); + + /** + * Evict the content viewer associated with a bfcache entry that has timed + * out. + */ + [noscript, notxpcom] + void evictExpiredDocumentViewerForEntry(in SHEntrySharedParentStatePtr aEntry); + + /** + * Evict all the content viewers in this session history + */ + void evictAllDocumentViewers(); + + /** + * 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 EvictDocumentViewersOrReplaceEntry(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..81fc444009 --- /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 OnDocumentViewerEvicted(in unsigned long aNumEvicted); +}; diff --git a/docshell/shistory/nsSHEntry.cpp b/docshell/shistory/nsSHEntry.cpp new file mode 100644 index 0000000000..8fc4f74844 --- /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 "nsIDocShellTreeItem.h" +#include "nsIDocumentViewer.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->SetIsExemptFromHTTPSFirstMode(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::EvictDocumentViewer() { + nsCOMPtr<nsIDocumentViewer> viewer = GetDocumentViewer(); + if (viewer) { + mShared->NotifyListenersDocumentViewerEvicted(); + // Drop the presentation state before destroying the viewer, so that + // document teardown is able to correctly persist the state. + SetDocumentViewer(nullptr); + SyncPresentationState(); + viewer->Destroy(); + } +} + +NS_IMETHODIMP +nsSHEntry::SetDocumentViewer(nsIDocumentViewer* aViewer) { + return GetState()->SetDocumentViewer(aViewer); +} + +NS_IMETHODIMP +nsSHEntry::GetDocumentViewer(nsIDocumentViewer** aResult) { + *aResult = GetState()->mDocumentViewer; + NS_IF_ADDREF(*aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsSHEntry::GetIsInBFCache(bool* aResult) { + *aResult = !!GetState()->mDocumentViewer; + 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..1a018575bd --- /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 EvictDocumentViewer(); + + 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..2c0e33ef62 --- /dev/null +++ b/docshell/shistory/nsSHEntryShared.cpp @@ -0,0 +1,365 @@ +/* -*- 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 "nsIDocumentViewer.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::NotifyListenersDocumentViewerEvicted() { + if (nsCOMPtr<nsISHistory> shistory = do_QueryReferent(mSHistory)) { + RefPtr<nsSHistory> nsshistory = static_cast<nsSHistory*>(shistory.get()); + nsshistory->NotifyListenersDocumentViewerEvicted(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 (mDocumentViewer) { + 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 (mDocumentViewer && 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 (mDocumentViewer) { + mDocumentViewer->ClearHistoryEntry(); + } + + RemoveFromExpirationTracker(); + mDocumentViewer = nullptr; + mSticky = true; + mWindowState = nullptr; + mViewerBounds.SetRect(0, 0, 0, 0); + mChildShells.Clear(); + mRefreshURIList = nullptr; + mEditorData = nullptr; +} + +nsresult nsSHEntryShared::SetDocumentViewer(nsIDocumentViewer* aViewer) { + MOZ_ASSERT(!aViewer || !mDocumentViewer, + "SHEntryShared already contains viewer"); + + if (mDocumentViewer || !aViewer) { + DropPresentationState(); + } + + // If we're setting mDocumentViewer 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()); + mDocumentViewer = aViewer; + + if (mDocumentViewer) { + // 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 = mDocumentViewer->GetDocument(); + if (mDocument) { + mDocument->SetBFCacheEntry(this); + mDocument->AddMutationObserver(this); + } + } + + return NS_OK; +} + +nsresult nsSHEntryShared::RemoveFromBFCacheSync() { + MOZ_ASSERT(mDocumentViewer && 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 mDocumentViewer. + nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer; + 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(mDocumentViewer && 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 mDocumentViewer & mDocument. Capture and + // release the references asynchronously so that the document doesn't get + // nuked mid-mutation. + nsCOMPtr<nsIDocumentViewer> viewer = mDocumentViewer; + RefPtr<dom::Document> document = mDocument; + RefPtr<nsSHEntryShared> self = this; + nsresult rv = mDocument->Dispatch(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; +} + +// Don't evict a page from bfcache for attribute mutations on NAC subtrees like +// scrollbars. +static bool IgnoreMutationForBfCache(const nsINode& aNode) { + for (const nsINode* node = &aNode; node; node = node->GetParentNode()) { + if (!node->ChromeOnlyAccess()) { + break; + } + // Make sure we find a scrollbar in the ancestor chain, to be safe. + if (node->IsXULElement(nsGkAtoms::scrollbar)) { + return true; + } + } + return false; +} + +void nsSHEntryShared::CharacterDataChanged(nsIContent* aContent, + const CharacterDataChangeInfo&) { + if (!IgnoreMutationForBfCache(*aContent)) { + RemoveFromBFCacheAsync(); + } +} + +void nsSHEntryShared::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + if (!IgnoreMutationForBfCache(*aElement)) { + RemoveFromBFCacheAsync(); + } +} + +void nsSHEntryShared::ContentAppended(nsIContent* aFirstNewContent) { + if (!IgnoreMutationForBfCache(*aFirstNewContent)) { + RemoveFromBFCacheAsync(); + } +} + +void nsSHEntryShared::ContentInserted(nsIContent* aChild) { + if (!IgnoreMutationForBfCache(*aChild)) { + RemoveFromBFCacheAsync(); + } +} + +void nsSHEntryShared::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (!IgnoreMutationForBfCache(*aChild)) { + RemoveFromBFCacheAsync(); + } +} diff --git a/docshell/shistory/nsSHEntryShared.h b/docshell/shistory/nsSHEntryShared.h new file mode 100644 index 0000000000..5ff855ead4 --- /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 nsIDocShellTreeItem; +class nsIDocumentViewer; +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 NotifyListenersDocumentViewerEvicted(); + + 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<nsIDocumentViewer> mDocumentViewer; + 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 SetDocumentViewer(nsIDocumentViewer* aViewer); +}; + +#endif diff --git a/docshell/shistory/nsSHistory.cpp b/docshell/shistory/nsSHistory.cpp new file mode 100644 index 0000000000..d69d78a9ec --- /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 "nsIDocShell.h" +#include "nsIDocumentViewer.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 constexpr 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::GloballyEvictDocumentViewers(); +} + +NS_IMETHODIMP +nsSHistoryObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "cacheservice:empty-cache") || + !strcmp(aTopic, "memory-pressure")) { + nsSHistory::GloballyEvictAllDocumentViewers(); + } + + return NS_OK; +} + +void nsSHistory::EvictDocumentViewerForEntry(nsISHEntry* aEntry) { + nsCOMPtr<nsIDocumentViewer> viewer = aEntry->GetDocumentViewer(); + 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. + NotifyListenersDocumentViewerEvicted(1); + aEntry->SetDocumentViewer(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::EvictDocumentViewerForEntry " + "destroying an nsFrameLoader.")); + NotifyListenersDocumentViewerEvicted(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::NotifyListenersDocumentViewerEvicted(uint32_t aNumEvicted) { + NotifyListeners(mListeners, [aNumEvicted](auto l) { + l->OnDocumentViewerEvicted(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::EvictOutOfRangeDocumentViewers(int32_t aIndex) { + MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug, + ("nsSHistory::EvictOutOfRangeDocumentViewers %i", aIndex)); + + // Check our per SHistory object limit in the currently navigated SHistory + EvictOutOfRangeWindowDocumentViewers(aIndex); + // Check our total limit across all SHistory objects + GloballyEvictDocumentViewers(); + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsSHistory::EvictDocumentViewersOrReplaceEntry(nsISHEntry* aNewSHEntry, + bool aReplace) { + if (!aReplace) { + int32_t curIndex; + GetIndex(&curIndex); + if (curIndex > -1) { + EvictOutOfRangeDocumentViewers(curIndex); + } + } else { + nsCOMPtr<nsISHEntry> rootSHEntry = nsSHistory::GetRootSHEntry(aNewSHEntry); + + int32_t index = GetIndexOfEntry(rootSHEntry); + if (index > -1) { + ReplaceEntry(index, rootSHEntry); + } + } +} + +NS_IMETHODIMP +nsSHistory::EvictAllDocumentViewers() { + // 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++) { + EvictDocumentViewerForEntry(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); + // EvictOutOfRangeDocumentViewers is called here explicitly to + // possibly evict the now in the bfcache document. + // HistoryCommitIndexAndLength might not have evicted that before the + // FrameLoader swap. + shistory->EvictOutOfRangeDocumentViewers(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::EvictOutOfRangeWindowDocumentViewers(int32_t aIndex) { + // XXX rename method to EvictDocumentViewersExceptAroundIndex, 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( + ("EvictOutOfRangeWindowDocumentViewers(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<nsIDocumentViewer> safeViewers; + nsTArray<RefPtr<nsFrameLoader>> safeFrameLoaders; + for (int32_t i = startSafeIndex; i <= endSafeIndex; i++) { + nsCOMPtr<nsIDocumentViewer> viewer = mEntries[i]->GetDocumentViewer(); + 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<nsIDocumentViewer> viewer = entry->GetDocumentViewer(); + if (viewer) { + if (safeViewers.IndexOf(viewer) == -1) { + EvictDocumentViewerForEntry(entry); + } + } else if (nsCOMPtr<SessionHistoryEntry> she = + do_QueryInterface(mEntries[i])) { + nsFrameLoader* frameLoader = she->GetFrameLoader(); + if (frameLoader) { + if (!safeFrameLoaders.Contains(frameLoader)) { + EvictDocumentViewerForEntry(entry); + } + } + } + } +} + +namespace { + +class EntryAndDistance { + public: + EntryAndDistance(nsSHistory* aSHistory, nsISHEntry* aEntry, uint32_t aDist) + : mSHistory(aSHistory), + mEntry(aEntry), + mViewer(aEntry->GetDocumentViewer()), + 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<nsIDocumentViewer> mViewer; + RefPtr<nsFrameLoader> mFrameLoader; + uint32_t mLastTouched; + int32_t mDistance; +}; + +} // namespace + +// static +void nsSHistory::GloballyEvictDocumentViewers() { + // 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::EvictDocumentViewers() + // often enough. Once we do call EvictDocumentViewers() 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<nsIDocumentViewer> viewer = entry->GetDocumentViewer(); + + bool found = false; + bool hasDocumentViewerOrFrameLoader = false; + if (viewer) { + hasDocumentViewerOrFrameLoader = 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 == viewer) { + 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()) { + hasDocumentViewerOrFrameLoader = 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 (hasDocumentViewerOrFrameLoader && !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)->EvictDocumentViewerForEntry(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::EvictExpiredDocumentViewerForEntry( + 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) { + EvictDocumentViewerForEntry(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::GloballyEvictAllDocumentViewers() { + int32_t maxViewers = sHistoryMaxTotalViewers; + sHistoryMaxTotalViewers = 0; + GloballyEvictDocumentViewers(); + 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->SetIsExemptFromHTTPSFirstMode(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..97bbe7f0a9 --- /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->EvictExpiredDocumentViewerForEntry(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 NotifyListenersDocumentViewerEvicted(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 EvictOutOfRangeWindowDocumentViewers(int32_t aIndex); + void EvictDocumentViewerForEntry(nsISHEntry* aEntry); + static void GloballyEvictDocumentViewers(); + static void GloballyEvictAllDocumentViewers(); + + // 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.toml b/docshell/test/browser/browser.toml new file mode 100644 index 0000000000..bcda46fd2e --- /dev/null +++ b/docshell/test/browser/browser.toml @@ -0,0 +1,326 @@ +[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", + "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_restore_scroll.js"] +https_first_disabled = true +support-files = [ + "file_backforward_restore_scroll.html", + "file_backforward_restore_scroll.html^headers^", +] + +["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"] + +["browser_badCertDomainFixup.js"] + +["browser_bfcache_copycommand.js"] +skip-if = ["os == 'linux' && bits == 64"] # Bug 1730593 + +["browser_browsingContext-01.js"] +https_first_disabled = true + +["browser_browsingContext-02.js"] +https_first_disabled = true + +["browser_browsingContext-embedder.js"] + +["browser_browsingContext-getAllBrowsingContextsInSubtree.js"] + +["browser_browsingContext-getWindowByName.js"] + +["browser_browsingContext-webProgress.js"] +skip-if = ["os == 'linux' && bits == 64 && !debug"] # Bug 1721261 +https_first_disabled = true + +["browser_browsing_context_attached.js"] +https_first_disabled = true + +["browser_browsing_context_discarded.js"] +https_first_disabled = true + +["browser_bug92473.js"] + +["browser_bug134911.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_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_bug1415918_beforeunload_options.js"] +https_first_disabled = true + +["browser_bug1543077-3.js"] + +["browser_bug1594938.js"] + +["browser_bug1622420.js"] +support-files = [ + "file_bug1622420.html", + "Bug1622420Child.sys.mjs", +] + +["browser_bug1648464-1.js"] + +["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_bug1688368-1.js"] + +["browser_bug1691153.js"] +https_first_disabled = true + +["browser_bug1705872.js"] + +["browser_bug1716290-1.js"] + +["browser_bug1716290-2.js"] + +["browser_bug1716290-3.js"] + +["browser_bug1716290-4.js"] + +["browser_bug1719178.js"] + +["browser_bug1736248-1.js"] + +["browser_bug1757005.js"] + +["browser_bug1769189.js"] + +["browser_bug1798780.js"] + +["browser_click_link_within_view_source.js"] + +["browser_cross_process_csp_inheritance.js"] +https_first_disabled = true + +["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_csp_uir.js"] +support-files = [ + "file_csp_uir.html", + "file_csp_uir_dummy.html", +] + +["browser_dataURI_unique_opaque_origin.js"] +https_first_disabled = true + +["browser_data_load_inherit_csp.js"] + +["browser_fall_back_to_https.js"] +https_first_disabled = true +skip-if = ["os == 'mac'"] + +["browser_frameloader_swap_with_bfcache.js"] + +["browser_history_triggeringprincipal_viewsource.js"] +https_first_disabled = true + +["browser_isInitialDocument.js"] +https_first_disabled = true + +["browser_loadURI_postdata.js"] + +["browser_multiple_pushState.js"] +https_first_disabled = true + +["browser_onbeforeunload_frame.js"] +support-files = ["head_browser_onbeforeunload.js"] + +["browser_onbeforeunload_navigation.js"] +skip-if = ["os == 'win' && !debug"] # bug 1300351 + +["browser_onbeforeunload_parent.js"] +support-files = ["head_browser_onbeforeunload.js"] + +["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_replace_while_loading.js"] +skip-if = [ + "os == 'linux' && bits == 64 && os_version == '18.04'", # Bug 1604237 + "os == 'win'", # Bug 1671794 +] + +["browser_tab_touch_events.js"] + +["browser_targetTopLevelLinkClicksToBlank.js"] + +["browser_title_in_session_history.js"] +skip-if = ["!sessionHistoryInParent"] + +["browser_ua_emulation.js"] + +["browser_uriFixupAlternateRedirects.js"] +https_first_disabled = true +support-files = ["redirect_to_example.sjs"] + +["browser_uriFixupIntegration.js"] + +["browser_viewsource_chrome_to_content.js"] + +["browser_viewsource_multipart.js"] +support-files = ["file_basic_multipart.sjs"] 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..dcf2d80af9 --- /dev/null +++ b/docshell/test/browser/browser_alternate_fixup_middle_click_link.js @@ -0,0 +1,52 @@ +/* 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, + /* wantLoad = */ null, + /* waitForLoad = */ true, + /* waitForAnyTab = */ false, + /* maybeErrorPage = */ true + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "a[href]", + { button: 1 }, + browser + ); + let tab = await newTabPromise; + let { browsingContext } = tab.linkedBrowser; + // 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..ca687bf528 --- /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.startLoadingURIString(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..4bf6ddcef3 --- /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.startLoadingURIString(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.startLoadingURIString(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..cd0015ac03 --- /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.startLoadingURIString(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..225242be60 --- /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.startLoadingURIString(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.startLoadingURIString(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..6b577a4d12 --- /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.startLoadingURIString(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.startLoadingURIString( + 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..e30706b745 --- /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.startLoadingURIString( + 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.startLoadingURIString( + 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..627abd90ba --- /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.startLoadingURIString(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..c32c38fe45 --- /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.startLoadingURIString(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..ff9e990a40 --- /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.startLoadingURIString(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..ed4969dae8 --- /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.startLoadingURIString(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..87d10b4526 --- /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.startLoadingURIString(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..6b257a9115 --- /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.startLoadingURIString(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..a9ad16ee35 --- /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.startLoadingURIString(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..33fca715cf --- /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.startLoadingURIString(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..152d4faedb --- /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.startLoadingURIString(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..d2b6370d03 --- /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.startLoadingURIString(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.startLoadingURIString(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..6fad3d39e6 --- /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.startLoadingURIString(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..12cc3e06fb --- /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.startLoadingURIString(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..7d64a781d1 --- /dev/null +++ b/docshell/test/browser/browser_fall_back_to_https.js @@ -0,0 +1,82 @@ +/* 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], + ["dom.security.https_first_schemeless", false], + ], + } + ); + + for (let test of bug1002724_tests) { + await test_one(test); + } + + // Test with HTTPS-First upgrading of schemeless enabled + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_schemeless", true]], + }); + + bug1002724_tests[0].expected = "https://example.com"; + await test_one(bug1002724_tests[0]); +}); 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..d9f5a41573 --- /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.startLoadingURIString(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..03265c8e3e --- /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.createAboutBlankDocumentViewer(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..e5bacfee06 --- /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.startLoadingURIString(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..f9371d59ce --- /dev/null +++ b/docshell/test/browser/browser_overlink.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +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 == + UrlbarTestUtils.trimURL("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..9d5a91bef9 --- /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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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_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..933af2a4af --- /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.startLoadingURIString(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..748cf4bce0 --- /dev/null +++ b/docshell/test/browser/browser_viewsource_multipart.js @@ -0,0 +1,47 @@ +/* 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.startLoadingURIString(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.startLoadingURIString( + 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/head.js b/docshell/test/browser/head.js new file mode 100644 index 0000000000..38f2528a2f --- /dev/null +++ b/docshell/test/browser/head.js @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * 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.startLoadingURIString(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..c5b676b6b8 --- /dev/null +++ b/docshell/test/chrome/bug113934_window.xhtml @@ -0,0 +1,156 @@ +<?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, + _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; + } + + if (this._addedDispatched) { + $("f1").removeEventListener("DOMLinkAdded", this); + $("f3").removeEventListener("DOMLinkAdded", this); + window.close(); + SimpleTest.finish(); + } + } + }; + + $("f1").addEventListener("DOMLinkAdded", DOMLinkListener); + $("f3").addEventListener("DOMLinkAdded", 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..ddbe2630b9 --- /dev/null +++ b/docshell/test/chrome/bug215405_window.xhtml @@ -0,0 +1,179 @@ +<?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.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + 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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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.startLoadingURIString(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..8bb050cebb --- /dev/null +++ b/docshell/test/chrome/bug293235_window.xhtml @@ -0,0 +1,120 @@ +<?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.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" + ); + + // 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..15b19f730a --- /dev/null +++ b/docshell/test/chrome/bug690056_window.xhtml @@ -0,0 +1,173 @@ +<?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); + + TestWindow.getBrowser().docShellIsActive = true; + + // 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" manualactiveness="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.toml b/docshell/test/chrome/chrome.toml new file mode 100644 index 0000000000..ee4d287247 --- /dev/null +++ b/docshell/test/chrome/chrome.toml @@ -0,0 +1,142 @@ +[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_bug89419.xhtml"] + +["test_bug92598.xhtml"] +support-files = [ + "92598_nostore.html", + "92598_nostore.html^headers^", + "bug92598_window.xhtml", +] + +["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"] + +["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"] + +["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_bug909218.html"] + +["test_docRedirect.xhtml"] + +["test_mozFrameType.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_viewsource_forbidden_in_iframe.xhtml"] +skip-if = ["true"] # bug 1019315 diff --git a/docshell/test/chrome/docshell_helpers.js b/docshell/test/chrome/docshell_helpers.js new file mode 100644 index 0000000000..7f16708087 --- /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.startLoadingURIString(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..4da580c117 --- /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.createAboutBlankDocumentViewer(systemPrincipal, systemPrincipal); + var win = docShell.docViewer.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..196d40e12d --- /dev/null +++ b/docshell/test/chrome/test_bug789773.xhtml @@ -0,0 +1,69 @@ +<?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.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + 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..56fdc1c5d2 --- /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.createAboutBlankDocumentViewer(system, system); + + progressListener.add(docShell, function(success) { + webNav.close(); + SimpleTest.is(success, true, "Received document redirect event"); + SimpleTest.finish(); + }); + + var win = docShell.docViewer.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.toml b/docshell/test/iframesandbox/mochitest.toml new file mode 100644 index 0000000000..a8bf4b1d72 --- /dev/null +++ b/docshell/test/iframesandbox/mochitest.toml @@ -0,0 +1,43 @@ +[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.html"] + +["test_top_navigation_by_location_exotic.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", + "http2", +] + 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..4ca088173c --- /dev/null +++ b/docshell/test/mochitest/double_submit.sjs @@ -0,0 +1,78 @@ +"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"); + 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..3701bf2cad --- /dev/null +++ b/docshell/test/mochitest/file_bug1121701_1.html @@ -0,0 +1,27 @@ +<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; + 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}; + 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..d97526f281 --- /dev/null +++ b/docshell/test/mochitest/file_bug1742865.sjs @@ -0,0 +1,75 @@ +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..f5e689a526 --- /dev/null +++ b/docshell/test/mochitest/file_bug1742865_outer.sjs @@ -0,0 +1,23 @@ +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..c310ec66f5 --- /dev/null +++ b/docshell/test/mochitest/file_bug1773192_2.html @@ -0,0 +1,19 @@ +<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); + setTimeout(() => { + // The test relies on this page not going into the BFCache, so that + // when we come back to it we load the URL from the replaceState + // instead. + window.blockBFCache = new RTCPeerConnection(); + document.forms[0].submit(); + }, 0); + </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_bug1850335_1.html b/docshell/test/mochitest/file_bug1850335_1.html new file mode 100644 index 0000000000..f317d2197a --- /dev/null +++ b/docshell/test/mochitest/file_bug1850335_1.html @@ -0,0 +1,23 @@ +<script> +addEventListener("load", ({ persisted }) => { + document.getElementById("input1").value = ""; +}); +addEventListener("pageshow", ({ persisted }) => { + let bc = new BroadcastChannel("bug1850335"); + + bc.addEventListener("message", ({ data: { cmd, arg } }) => { + if (cmd == "setValue") { + document.getElementById("input1").value = arg; + bc.postMessage({ value: document.getElementById("input1").value }); + } else if (cmd == "load") { + bc.close(); + location = arg; + } else if (cmd == "close") { + close(); + } + }); + + bc.postMessage({ persisted, value: document.getElementById("input1").value }); +}); +</script> +<input type="text" id="input1"></input> diff --git a/docshell/test/mochitest/file_bug1850335_2.html b/docshell/test/mochitest/file_bug1850335_2.html new file mode 100644 index 0000000000..e2c55a6a04 --- /dev/null +++ b/docshell/test/mochitest/file_bug1850335_2.html @@ -0,0 +1,18 @@ +<script> +addEventListener("pageshow", ({ persisted }) => { + window.blockBFCache = new RTCPeerConnection(); + + let bc = new BroadcastChannel("bug1850335"); + + bc.addEventListener("message", ({ data: { cmd, arg } }) => { + bc.close(); + if (cmd == "load") { + location = arg; + } else if (cmd == "back") { + history.back(); + } + }, { once: true }); + + bc.postMessage({ persisted }); +}); +</script> diff --git a/docshell/test/mochitest/file_bug1850335_3.html b/docshell/test/mochitest/file_bug1850335_3.html new file mode 100644 index 0000000000..bd9ef0af7e --- /dev/null +++ b/docshell/test/mochitest/file_bug1850335_3.html @@ -0,0 +1,7 @@ +<script> +addEventListener("pageshow", () => { + setTimeout(() => { + history.back(); + }, 0); +}); +</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..96fd84561c --- /dev/null +++ b/docshell/test/mochitest/form_submit_redirect.sjs @@ -0,0 +1,13 @@ +"use strict"; + +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.toml b/docshell/test/mochitest/mochitest.toml new file mode 100644 index 0000000000..8d593c9e36 --- /dev/null +++ b/docshell/test/mochitest/mochitest.toml @@ -0,0 +1,321 @@ +[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_bug385434.html"] + +["test_bug387979.html"] + +["test_bug402210.html"] +skip-if = [ + "http3", + "http2", +] + +["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 = [ + "os == 'win'", + "os == 'linux'", + "os == 'android'", + "headless", # Headless: bug 1410525 +] +support-files = ["file_bug511449.html"] + +["test_bug529119-1.html"] + +["test_bug529119-2.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug530396.html"] +support-files = [ + "bug530396-noref.sjs", + "bug530396-subframe.html", +] +skip-if = [ + "http3", + "http2", +] + +["test_bug540462.html"] +skip-if = ["os == '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", + "http2", +] + +["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_bug1422334.html"] +support-files = [ + "bug1422334_redirect.html", + "bug1422334_redirect.html^headers^", + "!/docshell/test/navigation/blank.html", +] + +["test_bug1450164.html"] + +["test_bug1507702.html"] + +["test_bug1645781.html"] +support-files = ["form_submit.sjs"] +skip-if = [ + "http3", + "http2", +] + +["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_bug1741132.html"] +support-files = ["file_bug1741132.html"] +skip-if = ["os == 'android' && !sessionHistoryInParent"] + +["test_bug1742865.html"] +support-files = [ + "file_bug1742865.sjs", + "file_bug1742865_outer.sjs", +] +skip-if = [ + "os == 'android' && debug && fission && verify", # Bug 1745937 + "http3", + "http2", +] + +["test_bug1743353.html"] +support-files = ["file_bug1743353.html"] + +["test_bug1747033.html"] +support-files = ["file_bug1747033.sjs"] +skip-if = [ + "http3", + "http2", +] + +["test_bug1773192.html"] +support-files = [ + "file_bug1773192_1.html", + "file_bug1773192_2.html", + "file_bug1773192_3.sjs", +] + +["test_bug1850335.html"] +support-files = [ + "file_bug1850335_1.html", + "file_bug1850335_2.html", + "file_bug1850335_3.html", +] + +["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", + "http2", +] + +["test_double_submit.html"] +support-files = [ + "clicker.html", + "double_submit.sjs", +] +skip-if = [ + "http3", + "http2", +] + +["test_forceinheritprincipal_overrule_owner.html"] +skip-if = [ + "http3", + "http2", +] + +["test_form_restoration.html"] +support-files = [ + "file_form_restoration_no_store.html", + "file_form_restoration_no_store.html^headers^", +] + +["test_framedhistoryframes.html"] +support-files = ["file_framedhistoryframes.html"] +skip-if = [ + "http3", + "http2", +] + +["test_iframe_srcdoc_to_remote.html"] +skip-if = [ + "http3", + "http2", +] + +["test_javascript_sandboxed_popup.html"] + +["test_load_during_reload.html"] +support-files = ["file_load_during_reload.html"] + +["test_navigate_after_pagehide.html"] +skip-if = [ + "http3", + "http2", +] + +["test_pushState_after_document_open.html"] + +["test_redirect_history.html"] +support-files = [ + "file_redirect_history.html", + "form_submit_redirect.sjs", +] +skip-if = [ + "http3", + "http2", +] + +["test_triggeringprincipal_location_seturi.html"] +skip-if = [ + "http3", + "http2", +] + +["test_windowedhistoryframes.html"] +skip-if = [ + "!debug && os == 'android'", # Bug 1573892 + "http3", + "http2", +] 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_bug1850335.html b/docshell/test/mochitest/test_bug1850335.html new file mode 100644 index 0000000000..546aab59c8 --- /dev/null +++ b/docshell/test/mochitest/test_bug1850335.html @@ -0,0 +1,72 @@ +<!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(); + + function waitForMessage(bc) { + return new Promise(resolve => { + bc.addEventListener("message", ({ data }) => { resolve(data); }, { once: true }); + }); + } + + async function runTest() { + let bc = new BroadcastChannel("bug1850335"); + + // Load the first page. + let waitForPage1 = waitForMessage(bc); + window.open("file_bug1850335_1.html", "_blank", "noopener"); + let { persisted, value: initial } = await waitForPage1; + + ok(!persisted, "Loaded first page"); + is(initial, "", "Initial value is empty string"); + + // Set the value of the input element in the first page. + let waitForValue = waitForMessage(bc); + bc.postMessage({ cmd: "setValue", arg: "ok" }); + let { value: expected }= await waitForValue; + is(expected, "ok", "Loaded first page"); + + // Load the second page (same origin with the first page). + let waitForPage2 = waitForMessage(bc); + bc.postMessage({ cmd: "load", arg: "file_bug1850335_2.html" }); + ({ persisted } = await waitForPage2); + + ok(!persisted, "Loaded second page (same-origin)"); + + // Load the third page (cross origin with the first and second pages). The + // third page will immediately do |history.back()| to go back to the + // second page. + waitForPage2 = waitForMessage(bc); + const crossOrigin = new URL("file_bug1850335_3.html", `https://example.com${location.pathname}`); + bc.postMessage({ cmd: "load", arg: crossOrigin.href }); + ({ persisted } = await waitForPage2); + + ok(!persisted, "Second page should not be in the BFCache"); + + // Go back to the first page. + waitForPage1 = waitForMessage(bc); + bc.postMessage({ cmd: "back" }); + let { persisted: fromBFCache, value: result } = await waitForPage1; + + ok(fromBFCache, "Page came from BFCache"); + is(result, expected, "Value wasn't cleared"); + + bc.postMessage({ cmd: "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_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..dac11853d0 --- /dev/null +++ b/docshell/test/mochitest/test_iframe_srcdoc_to_remote.html @@ -0,0 +1,45 @@ +<!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.effectiveIsolationStrategy() == SpecialPowers.ISOLATION_STRATEGY.IsolateEverything) { + 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..0cdf1633c1 --- /dev/null +++ b/docshell/test/moz.build @@ -0,0 +1,134 @@ +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/*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.toml b/docshell/test/navigation/browser.toml new file mode 100644 index 0000000000..f56b1dea12 --- /dev/null +++ b/docshell/test/navigation/browser.toml @@ -0,0 +1,28 @@ +[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_bug343515.js"] + +["browser_bug1757458.js"] + +["browser_ghistorymaxsize_is_0.js"] + +["browser_test-content-chromeflags.js"] +tags = "openwindow" + +["browser_test_bfcache_eviction.js"] + +["browser_test_shentry_wireframe.js"] +skip-if = ["!sessionHistoryInParent"] + +["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..958012f7eb --- /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.startLoadingURIString(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..aaf0df6a9c --- /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.startLoadingURIString( + 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.startLoadingURIString( + 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..87331b9bb9 --- /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.startLoadingURIString(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..5aac175ba4 --- /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 = { + OnDocumentViewerEvicted() { + 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 = { + OnDocumentViewerEvicted() { + 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.startLoadingURIString(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..49a74ff1e9 --- /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.startLoadingURIString(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..c6b13afd5e --- /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.startLoadingURIString(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..d81f948d89 --- /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.evictAllDocumentViewers(); + + 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..274084b03b --- /dev/null +++ b/docshell/test/navigation/file_meta_refresh.html @@ -0,0 +1,39 @@ +<html> + <head> + </head> + <body> + <script> + function addMetaRefresh() { + 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..29a1264fac --- /dev/null +++ b/docshell/test/navigation/file_reload_large_postdata.sjs @@ -0,0 +1,44 @@ +/* -*- 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" +); + +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_sessionstorage_across_coop.html b/docshell/test/navigation/file_sessionstorage_across_coop.html new file mode 100644 index 0000000000..44489b9e2f --- /dev/null +++ b/docshell/test/navigation/file_sessionstorage_across_coop.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <body> + <script> + addEventListener("load", () => { + let bc = new BroadcastChannel("testChannel"); + bc.postMessage(sessionStorage.getItem("testItem")); + window.close(); + }); + </script> + </body> +</html> diff --git a/docshell/test/navigation/file_sessionstorage_across_coop.html^headers^ b/docshell/test/navigation/file_sessionstorage_across_coop.html^headers^ new file mode 100644 index 0000000000..46ad58d83b --- /dev/null +++ b/docshell/test/navigation/file_sessionstorage_across_coop.html^headers^ @@ -0,0 +1 @@ +Cross-Origin-Opener-Policy: same-origin 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..4685fea7b7 --- /dev/null +++ b/docshell/test/navigation/iframe.html @@ -0,0 +1,8 @@ +<html> +<body> +<script> +var src = window.location.hash.substring(1); +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.toml b/docshell/test/navigation/mochitest.toml new file mode 100644 index 0000000000..ce0fa0d18f --- /dev/null +++ b/docshell/test/navigation/mochitest.toml @@ -0,0 +1,359 @@ +[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_blockBFCache.html"] +support-files = [ + "file_blockBFCache.html", + "slow.sjs", + "iframe_slow_onload.html", + "iframe_slow_onload_inner.html", +] +skip-if = [ + "http3", + "http2", +] + +["test_bug13871.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug270414.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug278916.html"] + +["test_bug279495.html"] + +["test_bug344861.html"] +skip-if = ["os == 'android'"] + +["test_bug386782.html"] + +["test_bug430624.html"] + +["test_bug430723.html"] +skip-if = [ + "!debug && (os == 'mac' || os == 'win')", # Bug 874423 + "http3", + "http2", +] + +["test_bug1300461.html"] + +["test_bug1326251.html"] +skip-if = [ + "os == 'android'", + "sessionHistoryInParent", # It relies on the bfcache +] + +["test_bug1364364.html"] +skip-if = ["os == 'android'"] # Bug 1560378 + +["test_bug1375833.html"] + +["test_bug1379762.html"] + +["test_bug1536471.html"] +support-files = ["file_bug1536471.html"] + +["test_bug1583110.html"] +support-files = ["file_bug1583110.html"] + +["test_bug1609475.html"] + +["test_bug1699721.html"] +skip-if = ["!fission"] # tests fission-only process switching behaviour + +["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_child.html"] + +["test_contentpolicy_block_window.html"] + +["test_docshell_gotoindex.html"] +support-files = ["file_docshell_gotoindex.html"] + +["test_dynamic_frame_forward_back.html"] + +["test_evict_from_bfcache.html"] +support-files = ["file_evict_from_bfcache.html"] + +["test_fragment_handling_during_load.html"] + +["test_grandchild.html"] +skip-if = [ + "http3", + "http2", +] + +["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_nested_frames.html"] +skip-if = [ + "http3", + "http2", +] + +["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", + "http2", +] + +["test_online_offline_bfcache.html"] +support-files = ["file_online_offline_bfcache.html"] + +["test_open_javascript_noopener.html"] + +["test_opener.html"] +skip-if = ["true"] # Bug 1572299, Bug 1716402, Bug 1797751, Bug 1781601 + +["test_performance_navigation.html"] + +["test_popup-navigates-children.html"] +skip-if = [ + "http3", + "http2", +] + +["test_rate_limit_location_change.html"] + +["test_recursive_frames.html"] +skip-if = [ + "http3", + "http2", +] + +["test_reload.html"] +support-files = ["file_reload.html"] + +["test_reload_large_postdata.html"] +support-files = ["file_reload_large_postdata.sjs"] + +["test_reload_nonbfcached_srcdoc.html"] +support-files = ["file_reload_nonbfcached_srcdoc.sjs"] +skip-if = [ + "http3", + "http2", +] + +["test_reserved.html"] +skip-if = ["debug"] # bug 1263213 + +["test_same_url.html"] +support-files = ["file_same_url.html"] + +["test_scrollRestoration.html"] + +["test_session_history_entry_cleanup.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_sessionhistory_document_write.html"] + +["test_sessionhistory_iframe_removal.html"] +support-files = ["file_sessionhistory_iframe_removal.html"] + +["test_sessionstorage_across_coop.html"] +support-files = [ + "file_sessionstorage_across_coop.html", + "file_sessionstorage_across_coop.html^headers^", +] + +["test_shiftReload_and_pushState.html"] + +["test_ship_beforeunload_fired.html"] +support-files = ["file_ship_beforeunload_fired.html"] +skip-if = [ + "!sessionHistoryInParent", + "http3", + "http2", +] + +["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_sibling-matching-parent.html"] + +["test_sibling-off-domain.html"] +skip-if = [ + "http3", + "http2", +] + +["test_state_size.html"] + +["test_static_and_dynamic.html"] +skip-if = ["true"] # This was disabled for a few years now anyway, bug 1677544 + +["test_triggeringprincipal_frame_nav.html"] +skip-if = [ + "http3", + "http2", +] + +["test_triggeringprincipal_frame_same_origin_nav.html"] +skip-if = [ + "http3", + "http2", +] + +["test_triggeringprincipal_iframe_iframe_window_open.html"] +skip-if = [ + "http3", + "http2", +] + +["test_triggeringprincipal_parent_iframe_window_open.html"] +skip-if = [ + "http3", + "http2", +] + +["test_triggeringprincipal_window_open.html"] +skip-if = [ + "http3", + "http2", +] diff --git a/docshell/test/navigation/navigate.html b/docshell/test/navigation/navigate.html new file mode 100644 index 0000000000..eed1eb1c18 --- /dev/null +++ b/docshell/test/navigation/navigate.html @@ -0,0 +1,37 @@ +<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]; +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..97eb9b76e1 --- /dev/null +++ b/docshell/test/navigation/open.html @@ -0,0 +1,9 @@ +<html> +<body> +<script> +var target = window.location.hash.substring(1); +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..5cfbdb38f7 --- /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.effectiveIsolationStrategy() == SpecialPowers.ISOLATION_STRATEGY.IsolateEverything) { + 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..c6ae8e88d3 --- /dev/null +++ b/docshell/test/navigation/test_bug1699721.html @@ -0,0 +1,121 @@ +<!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() { + // Induce a process switching behavior for example.com + // with isolateHighValue isolation strategy + // (because we test specifically process switching behavior here) + await SpecialPowers.pushPermissions([ + { + type: "highValueCOOP", + allow: SpecialPowers.Ci.nsIPermissionManager.ALLOW_ACTION, + context: "https://example.com", + } + ]); + + 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, "https://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..1d2829750d --- /dev/null +++ b/docshell/test/navigation/test_bug270414.html @@ -0,0 +1,95 @@ +<!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(); + window0.document.write(headerHTML); + window0.document.write("<script>navigateByLocation(opener.frames[0])<\/script>"); + window0.document.write(footerHTML); + window0.document.close(); + } +} + +function testChild1() { + if (!window.window1) { + window1 = window.open("", "window1", "width=10,height=10"); + window1.document.open(); + window1.document.write(headerHTML); + window1.document.write("<script>navigateByOpen('child1');<\/script>"); + window1.document.write(footerHTML); + window1.document.close(); + } +} + +function testChild2() { + if (!window.window2) { + window2 = window.open("", "window2", "width=10,height=10"); + window2.document.open(); + window2.document.write(headerHTML); + window2.document.write("<script>navigateByForm('child2');<\/script>"); + window2.document.write(footerHTML); + window2.document.close(); + } +} + +function testChild3() { + if (!window.window3) { + window3 = window.open("", "window3", "width=10,height=10"); + window3.document.open(); + window3.document.write(headerHTML); + window3.document.write("<script>navigateByHyperlink('child3');<\/script>"); + 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..7dc73e8574 --- /dev/null +++ b/docshell/test/navigation/test_contentpolicy_block_window.html @@ -0,0 +1,97 @@ +<!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 + 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) { + 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) { + 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_sessionstorage_across_coop.html b/docshell/test/navigation/test_sessionstorage_across_coop.html new file mode 100644 index 0000000000..1195f8767c --- /dev/null +++ b/docshell/test/navigation/test_sessionstorage_across_coop.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1790666 +--> +<head> + <title>Test for Bug 1790666</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=1790666">Mozilla Bug 1790666</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + async function singleTest(testValue, description) { + info(`Starting test: ${description}`); + + let bc = new BroadcastChannel("testChannel"); + let promise = new Promise(resolve => { + bc.addEventListener("message", event => { + info(`received message from testChannel: ${event.data}`); + resolve(event.data); + }, { once: true }); + }); + + info("Opening pop-up"); + let popup = window.open("", "_blank"); + popup.sessionStorage.setItem("testItem", testValue); + + info("Navigating pop-up to COOP page"); + popup.location = new URL("file_sessionstorage_across_coop.html", window.location); + + let newValue = await promise; + is(newValue, testValue, "Value matches expected value"); + } + + add_task(async function() { + // Cross-Origin-Opener-Policy is only supported in secure contexts, so + // make the test a secure context. + await SpecialPowers.pushPrefEnv({ + "set": [["dom.securecontext.allowlist", "mochi.test"]], + }); + + await singleTest("short test value", "short test value"); + + let longValue = "A".repeat(SpecialPowers.getIntPref("browser.sessionstore.dom_storage_limit") * 2); + await singleTest(longValue, "long test value"); + }); + </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..1d74bd61c7 --- /dev/null +++ b/docshell/test/unit/head_docshell.js @@ -0,0 +1,103 @@ +/* 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", + HttpServer: "resource://testing-common/httpd.sys.mjs", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +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..794ee8cfa2 --- /dev/null +++ b/docshell/test/unit/test_URIFixup.js @@ -0,0 +1,169 @@ +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", + }, + { + // Spaces before @ are valid if it appears after the domain. + wrong: "example.com/ @test.com", + fixed: "http://example.com/%20@test.com", + noPrefValue: "http://example.com/%20@test.com", + }, +]; + +var dontFixURIs = [ + { + input: " leadingSpaceUsername@example.com/", + testInfo: "dont fix usernames with leading space", + }, + { + input: "trailingSpacerUsername @example.com/", + testInfo: "dont fix usernames with trailing space", + }, + { + input: "multiple words username@example.com/", + testInfo: "dont fix usernames with multiple spaces", + }, + { + input: "one spaceTwo SpacesThree Spaces@example.com/", + testInfo: "dont match multiple consecutive spaces", + }, + { + input: " dontMatchCredentialsWithSpaces: secret password @example.com/", + testInfo: "dont fix credentials with spaces", + }, +]; + +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); + } +}); + +add_task(function test_dont_fix_uris() { + let dontFixLength = dontFixURIs.length; + for (let i = 0; i < dontFixLength; i++) { + let testCase = dontFixURIs[i]; + Assert.throws( + () => { + Services.uriFixup.getFixupURIInfo( + testCase.input, + Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS + ); + }, + /NS_ERROR_MALFORMED_URI/, + testCase.testInfo + ); + } +}); 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..ec56af1c9b --- /dev/null +++ b/docshell/test/unit/test_URIFixup_info.js @@ -0,0 +1,1079 @@ +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const kForceDNSLookup = "browser.fixup.dns_first_for_single_words"; + +// 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_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", + keywordLookup: true, + }, + { + input: "-47.6182,-23.51", + keywordLookup: 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 { + const homeDir = Services.dirsvc.get("Home", Ci.nsIFile).path; + const homeBase = AppConstants.platform == "macosx" ? "/Users" : "/home"; + + testcases.push({ + input: "~", + fixedURI: `file://${homeDir}`, + protocolChange: true, + }); + testcases.push({ + input: "~/foo", + fixedURI: `file://${homeDir}/foo`, + protocolChange: true, + }); + testcases.push({ + input: "~foo", + fixedURI: `file://${homeBase}/foo`, + protocolChange: true, + }); + testcases.push({ + input: "~foo/bar", + fixedURI: `file://${homeBase}/foo/bar`, + protocolChange: true, + }); + 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(); + gSingleWordDNSLookup = true; + await do_single_test_run(); + gSingleWordDNSLookup = false; + await Services.search.setDefault( + Services.search.getEngineByName(kPostSearchEngineID), + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await do_single_test_run(); +}); + +async function do_single_test_run() { + Services.prefs.setBoolPref(kForceDNSLookup, gSingleWordDNSLookup); + 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") + + ")" + ); + + 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 makeAlternativeURI = + flags & Services.uriFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI; + + 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..ee1876b4b9 --- /dev/null +++ b/docshell/test/unit/test_subframe_stop_after_parent_error.js @@ -0,0 +1,145 @@ +"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_NETWORK + ); + }); +} + +async function runTest(waitForErrorPage) { + let page = await XPCShellContentUtils.loadContentPage("about:blank"); + + // 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 + ); + + await page.spawn( + [topFrameRequest.url, subFrameRequest.url], + function (topFrameUrl, subFrameRequestUrl) { + // Create a frame with a source URL which will not return a response + // before we cancel it with an error. + let doc = this.content.document; + let frame = doc.createElement("iframe"); + frame.src = topFrameUrl; + 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 = subFrameRequestUrl; + 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(page.browsingContext.webProgress); + + // 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(() => + page.browsingContext.children[0]?.currentWindowGlobal?.documentURI?.spec.startsWith( + "about:neterror?" + ) + ); + } + + 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.toml b/docshell/test/unit/xpcshell.toml new file mode 100644 index 0000000000..abc775f1ae --- /dev/null +++ b/docshell/test/unit/xpcshell.toml @@ -0,0 +1,43 @@ +[DEFAULT] +head = "head_docshell.js" +support-files = [ + "data/engine.xml", + "data/enginePost.xml", + "data/enginePrivate.xml", +] + +["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_info.js"] +skip-if = ["os == 'android'"] + +["test_URIFixup_search.js"] +skip-if = ["os == 'android'"] + +["test_allowJavascript.js"] +skip-if = ["os == 'android'"] +support-files = [ + "AllowJavascriptChild.sys.mjs", + "AllowJavascriptParent.sys.mjs", +] + +["test_browsing_context_structured_clone.js"] + +["test_bug442584.js"] + +["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"] 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.toml b/docshell/test/unit_ipc/xpcshell.toml new file mode 100644 index 0000000000..9ddc971a24 --- /dev/null +++ b/docshell/test/unit_ipc/xpcshell.toml @@ -0,0 +1,8 @@ +[DEFAULT] +head = "" +skip-if = ["os == 'android'"] + +["test_pb_notification_ipc.js"] +# Bug 751575: Perma-fails with: command timed out: 1200 seconds without output +skip-if = ["true"] + |