summaryrefslogtreecommitdiffstats
path: root/docshell/shistory/nsSHistory.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'docshell/shistory/nsSHistory.cpp')
-rw-r--r--docshell/shistory/nsSHistory.cpp2410
1 files changed, 2410 insertions, 0 deletions
diff --git a/docshell/shistory/nsSHistory.cpp b/docshell/shistory/nsSHistory.cpp
new file mode 100644
index 0000000000..214646dde9
--- /dev/null
+++ b/docshell/shistory/nsSHistory.cpp
@@ -0,0 +1,2410 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSHistory.h"
+
+#include <algorithm>
+
+#include "nsContentUtils.h"
+#include "nsCOMArray.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDocShell.h"
+#include "nsFrameLoaderOwner.h"
+#include "nsHashKeys.h"
+#include "nsIContentViewer.h"
+#include "nsIDocShell.h"
+#include "nsDocShellLoadState.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsILayoutHistoryState.h"
+#include "nsIObserverService.h"
+#include "nsISHEntry.h"
+#include "nsISHistoryListener.h"
+#include "nsIURI.h"
+#include "nsIXULRuntime.h"
+#include "nsNetUtil.h"
+#include "nsTHashMap.h"
+#include "nsSHEntry.h"
+#include "SessionHistoryEntry.h"
+#include "nsTArray.h"
+#include "prsystem.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/RemoteWebProgressRequest.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProcessPriorityManager.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_fission.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "nsIWebNavigation.h"
+#include "nsDocShellLoadTypes.h"
+#include "base/process.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+#define PREF_SHISTORY_SIZE "browser.sessionhistory.max_entries"
+#define PREF_SHISTORY_MAX_TOTAL_VIEWERS \
+ "browser.sessionhistory.max_total_viewers"
+#define CONTENT_VIEWER_TIMEOUT_SECONDS \
+ "browser.sessionhistory.contentViewerTimeout"
+// Observe fission.bfcacheInParent so that BFCache can be enabled/disabled when
+// the pref is changed.
+#define PREF_FISSION_BFCACHEINPARENT "fission.bfcacheInParent"
+
+// Default this to time out unused content viewers after 30 minutes
+#define CONTENT_VIEWER_TIMEOUT_SECONDS_DEFAULT (30 * 60)
+
+static const char* kObservedPrefs[] = {PREF_SHISTORY_SIZE,
+ PREF_SHISTORY_MAX_TOTAL_VIEWERS,
+ PREF_FISSION_BFCACHEINPARENT, nullptr};
+
+static int32_t gHistoryMaxSize = 50;
+
+// List of all SHistory objects, used for content viewer cache eviction.
+// When being destroyed, this helper removes everything from the list to avoid
+// assertions when we leak.
+struct ListHelper {
+#ifdef DEBUG
+ ~ListHelper() { mList.clear(); }
+#endif // DEBUG
+
+ LinkedList<nsSHistory> mList;
+};
+
+static ListHelper gSHistoryList;
+// Max viewers allowed total, across all SHistory objects - negative default
+// means we will calculate how many viewers to cache based on total memory
+int32_t nsSHistory::sHistoryMaxTotalViewers = -1;
+
+// A counter that is used to be able to know the order in which
+// entries were touched, so that we can evict older entries first.
+static uint32_t gTouchCounter = 0;
+
+extern mozilla::LazyLogModule gSHLog;
+
+LazyLogModule gSHistoryLog("nsSHistory");
+
+#define LOG(format) MOZ_LOG(gSHistoryLog, mozilla::LogLevel::Debug, format)
+
+extern mozilla::LazyLogModule gPageCacheLog;
+extern mozilla::LazyLogModule gSHIPBFCacheLog;
+
+// This macro makes it easier to print a log message which includes a URI's
+// spec. Example use:
+//
+// nsIURI *uri = [...];
+// LOG_SPEC(("The URI is %s.", _spec), uri);
+//
+#define LOG_SPEC(format, uri) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gSHistoryLog, LogLevel::Debug)) { \
+ nsAutoCString _specStr("(null)"_ns); \
+ if (uri) { \
+ _specStr = uri->GetSpecOrDefault(); \
+ } \
+ const char* _spec = _specStr.get(); \
+ LOG(format); \
+ } \
+ PR_END_MACRO
+
+// This macro makes it easy to log a message including an SHEntry's URI.
+// For example:
+//
+// nsCOMPtr<nsISHEntry> shentry = [...];
+// LOG_SHENTRY_SPEC(("shentry %p has uri %s.", shentry.get(), _spec), shentry);
+//
+#define LOG_SHENTRY_SPEC(format, shentry) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gSHistoryLog, LogLevel::Debug)) { \
+ nsCOMPtr<nsIURI> uri = shentry->GetURI(); \
+ LOG_SPEC(format, uri); \
+ } \
+ PR_END_MACRO
+
+// Calls a F on all registered session history listeners.
+template <typename F>
+static void NotifyListeners(nsAutoTObserverArray<nsWeakPtr, 2>& aListeners,
+ F&& f) {
+ for (const nsWeakPtr& weakPtr : aListeners.EndLimitedRange()) {
+ nsCOMPtr<nsISHistoryListener> listener = do_QueryReferent(weakPtr);
+ if (listener) {
+ f(listener);
+ }
+ }
+}
+
+class MOZ_STACK_CLASS SHistoryChangeNotifier {
+ public:
+ explicit SHistoryChangeNotifier(nsSHistory* aHistory) {
+ // If we're already in an update, the outermost change notifier will
+ // update browsing context in the destructor.
+ if (!aHistory->HasOngoingUpdate()) {
+ aHistory->SetHasOngoingUpdate(true);
+ mSHistory = aHistory;
+ }
+ }
+
+ ~SHistoryChangeNotifier() {
+ if (mSHistory) {
+ MOZ_ASSERT(mSHistory->HasOngoingUpdate());
+ mSHistory->SetHasOngoingUpdate(false);
+
+ RefPtr<BrowsingContext> rootBC = mSHistory->GetBrowsingContext();
+ if (mozilla::SessionHistoryInParent() && rootBC) {
+ rootBC->Canonical()->HistoryCommitIndexAndLength();
+ }
+ }
+ }
+
+ RefPtr<nsSHistory> mSHistory;
+};
+
+enum HistCmd { HIST_CMD_GOTOINDEX, HIST_CMD_RELOAD };
+
+class nsSHistoryObserver final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ nsSHistoryObserver() {}
+
+ static void PrefChanged(const char* aPref, void* aSelf);
+ void PrefChanged(const char* aPref);
+
+ protected:
+ ~nsSHistoryObserver() {}
+};
+
+StaticRefPtr<nsSHistoryObserver> gObserver;
+
+NS_IMPL_ISUPPORTS(nsSHistoryObserver, nsIObserver)
+
+// static
+void nsSHistoryObserver::PrefChanged(const char* aPref, void* aSelf) {
+ static_cast<nsSHistoryObserver*>(aSelf)->PrefChanged(aPref);
+}
+
+void nsSHistoryObserver::PrefChanged(const char* aPref) {
+ nsSHistory::UpdatePrefs();
+ nsSHistory::GloballyEvictContentViewers();
+}
+
+NS_IMETHODIMP
+nsSHistoryObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, "cacheservice:empty-cache") ||
+ !strcmp(aTopic, "memory-pressure")) {
+ nsSHistory::GloballyEvictAllContentViewers();
+ }
+
+ return NS_OK;
+}
+
+void nsSHistory::EvictContentViewerForEntry(nsISHEntry* aEntry) {
+ nsCOMPtr<nsIContentViewer> viewer = aEntry->GetContentViewer();
+ if (viewer) {
+ LOG_SHENTRY_SPEC(("Evicting content viewer 0x%p for "
+ "owning SHEntry 0x%p at %s.",
+ viewer.get(), aEntry, _spec),
+ aEntry);
+
+ // Drop the presentation state before destroying the viewer, so that
+ // document teardown is able to correctly persist the state.
+ NotifyListenersContentViewerEvicted(1);
+ aEntry->SetContentViewer(nullptr);
+ aEntry->SyncPresentationState();
+ viewer->Destroy();
+ } else if (nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aEntry)) {
+ if (RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader()) {
+ nsCOMPtr<nsFrameLoaderOwner> owner =
+ do_QueryInterface(frameLoader->GetOwnerContent());
+ RefPtr<nsFrameLoader> currentFrameLoader;
+ if (owner) {
+ currentFrameLoader = owner->GetFrameLoader();
+ }
+
+ // Only destroy non-current frameloader when evicting from the bfcache.
+ if (currentFrameLoader != frameLoader) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ ("nsSHistory::EvictContentViewerForEntry "
+ "destroying an nsFrameLoader."));
+ NotifyListenersContentViewerEvicted(1);
+ she->SetFrameLoader(nullptr);
+ frameLoader->Destroy();
+ }
+ }
+ }
+
+ // When dropping bfcache, we have to remove associated dynamic entries as
+ // well.
+ int32_t index = GetIndexOfEntry(aEntry);
+ if (index != -1) {
+ RemoveDynEntries(index, aEntry);
+ }
+}
+
+nsSHistory::nsSHistory(BrowsingContext* aRootBC)
+ : mRootBC(aRootBC->Id()),
+ mHasOngoingUpdate(false),
+ mIndex(-1),
+ mRequestedIndex(-1),
+ mRootDocShellID(aRootBC->GetHistoryID()) {
+ static bool sCalledStartup = false;
+ if (!sCalledStartup) {
+ Startup();
+ sCalledStartup = true;
+ }
+
+ // Add this new SHistory object to the list
+ gSHistoryList.mList.insertBack(this);
+
+ // Init mHistoryTracker on setting mRootBC so we can bind its event
+ // target to the tabGroup.
+ mHistoryTracker = mozilla::MakeUnique<HistoryTracker>(
+ this,
+ mozilla::Preferences::GetUint(CONTENT_VIEWER_TIMEOUT_SECONDS,
+ CONTENT_VIEWER_TIMEOUT_SECONDS_DEFAULT),
+ GetCurrentSerialEventTarget());
+}
+
+nsSHistory::~nsSHistory() {
+ // Clear mEntries explicitly here so that the destructor of the entries
+ // can still access nsSHistory in a reasonable way.
+ mEntries.Clear();
+}
+
+NS_IMPL_ADDREF(nsSHistory)
+NS_IMPL_RELEASE(nsSHistory)
+
+NS_INTERFACE_MAP_BEGIN(nsSHistory)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISHistory)
+ NS_INTERFACE_MAP_ENTRY(nsISHistory)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END
+
+// static
+uint32_t nsSHistory::CalcMaxTotalViewers() {
+// This value allows tweaking how fast the allowed amount of content viewers
+// grows with increasing amounts of memory. Larger values mean slower growth.
+#ifdef ANDROID
+# define MAX_TOTAL_VIEWERS_BIAS 15.9
+#else
+# define MAX_TOTAL_VIEWERS_BIAS 14
+#endif
+
+ // Calculate an estimate of how many ContentViewers we should cache based
+ // on RAM. This assumes that the average ContentViewer is 4MB (conservative)
+ // and caps the max at 8 ContentViewers
+ //
+ // TODO: Should we split the cache memory betw. ContentViewer caching and
+ // nsCacheService?
+ //
+ // RAM | ContentViewers | on Android
+ // -------------------------------------
+ // 32 Mb 0 0
+ // 64 Mb 1 0
+ // 128 Mb 2 0
+ // 256 Mb 3 1
+ // 512 Mb 5 2
+ // 768 Mb 6 2
+ // 1024 Mb 8 3
+ // 2048 Mb 8 5
+ // 3072 Mb 8 7
+ // 4096 Mb 8 8
+ uint64_t bytes = PR_GetPhysicalMemorySize();
+
+ if (bytes == 0) {
+ return 0;
+ }
+
+ // Conversion from unsigned int64_t to double doesn't work on all platforms.
+ // We need to truncate the value at INT64_MAX to make sure we don't
+ // overflow.
+ if (bytes > INT64_MAX) {
+ bytes = INT64_MAX;
+ }
+
+ double kBytesD = (double)(bytes >> 10);
+
+ // This is essentially the same calculation as for nsCacheService,
+ // except that we divide the final memory calculation by 4, since
+ // we assume each ContentViewer takes on average 4MB
+ uint32_t viewers = 0;
+ double x = std::log(kBytesD) / std::log(2.0) - MAX_TOTAL_VIEWERS_BIAS;
+ if (x > 0) {
+ viewers = (uint32_t)(x * x - x + 2.001); // add .001 for rounding
+ viewers /= 4;
+ }
+
+ // Cap it off at 8 max
+ if (viewers > 8) {
+ viewers = 8;
+ }
+ return viewers;
+}
+
+// static
+void nsSHistory::UpdatePrefs() {
+ Preferences::GetInt(PREF_SHISTORY_SIZE, &gHistoryMaxSize);
+ if (mozilla::SessionHistoryInParent() && !mozilla::BFCacheInParent()) {
+ sHistoryMaxTotalViewers = 0;
+ return;
+ }
+
+ Preferences::GetInt(PREF_SHISTORY_MAX_TOTAL_VIEWERS,
+ &sHistoryMaxTotalViewers);
+ // If the pref is negative, that means we calculate how many viewers
+ // we think we should cache, based on total memory
+ if (sHistoryMaxTotalViewers < 0) {
+ sHistoryMaxTotalViewers = CalcMaxTotalViewers();
+ }
+}
+
+// static
+nsresult nsSHistory::Startup() {
+ UpdatePrefs();
+
+ // The goal of this is to unbreak users who have inadvertently set their
+ // session history size to less than the default value.
+ int32_t defaultHistoryMaxSize =
+ Preferences::GetInt(PREF_SHISTORY_SIZE, 50, PrefValueKind::Default);
+ if (gHistoryMaxSize < defaultHistoryMaxSize) {
+ gHistoryMaxSize = defaultHistoryMaxSize;
+ }
+
+ // Allow the user to override the max total number of cached viewers,
+ // but keep the per SHistory cached viewer limit constant
+ if (!gObserver) {
+ gObserver = new nsSHistoryObserver();
+ Preferences::RegisterCallbacks(nsSHistoryObserver::PrefChanged,
+ kObservedPrefs, gObserver.get());
+
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ if (obsSvc) {
+ // Observe empty-cache notifications so tahat clearing the disk/memory
+ // cache will also evict all content viewers.
+ obsSvc->AddObserver(gObserver, "cacheservice:empty-cache", false);
+
+ // Same for memory-pressure notifications
+ obsSvc->AddObserver(gObserver, "memory-pressure", false);
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+void nsSHistory::Shutdown() {
+ if (gObserver) {
+ Preferences::UnregisterCallbacks(nsSHistoryObserver::PrefChanged,
+ kObservedPrefs, gObserver.get());
+
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ if (obsSvc) {
+ obsSvc->RemoveObserver(gObserver, "cacheservice:empty-cache");
+ obsSvc->RemoveObserver(gObserver, "memory-pressure");
+ }
+ gObserver = nullptr;
+ }
+}
+
+// static
+already_AddRefed<nsISHEntry> nsSHistory::GetRootSHEntry(nsISHEntry* aEntry) {
+ nsCOMPtr<nsISHEntry> rootEntry = aEntry;
+ nsCOMPtr<nsISHEntry> result = nullptr;
+ while (rootEntry) {
+ result = rootEntry;
+ rootEntry = result->GetParent();
+ }
+
+ return result.forget();
+}
+
+// static
+nsresult nsSHistory::WalkHistoryEntries(nsISHEntry* aRootEntry,
+ BrowsingContext* aBC,
+ WalkHistoryEntriesFunc aCallback,
+ void* aData) {
+ NS_ENSURE_TRUE(aRootEntry, NS_ERROR_FAILURE);
+
+ int32_t childCount = aRootEntry->GetChildCount();
+ for (int32_t i = 0; i < childCount; i++) {
+ nsCOMPtr<nsISHEntry> childEntry;
+ aRootEntry->GetChildAt(i, getter_AddRefs(childEntry));
+ if (!childEntry) {
+ // childEntry can be null for valid reasons, for example if the
+ // docshell at index i never loaded anything useful.
+ // Remember to clone also nulls in the child array (bug 464064).
+ aCallback(nullptr, nullptr, i, aData);
+ continue;
+ }
+
+ BrowsingContext* childBC = nullptr;
+ if (aBC) {
+ for (BrowsingContext* child : aBC->Children()) {
+ // If the SH pref is on and we are in the parent process, update
+ // canonical BC directly
+ bool foundChild = false;
+ if (mozilla::SessionHistoryInParent() && XRE_IsParentProcess()) {
+ if (child->Canonical()->HasHistoryEntry(childEntry)) {
+ childBC = child;
+ foundChild = true;
+ }
+ }
+
+ nsDocShell* docshell = static_cast<nsDocShell*>(child->GetDocShell());
+ if (docshell && docshell->HasHistoryEntry(childEntry)) {
+ childBC = docshell->GetBrowsingContext();
+ foundChild = true;
+ }
+
+ // XXX Simplify this once the old and new session history
+ // implementations don't run at the same time.
+ if (foundChild) {
+ break;
+ }
+ }
+ }
+
+ nsresult rv = aCallback(childEntry, childBC, i, aData);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+// callback data for WalkHistoryEntries
+struct MOZ_STACK_CLASS CloneAndReplaceData {
+ CloneAndReplaceData(uint32_t aCloneID, nsISHEntry* aReplaceEntry,
+ bool aCloneChildren, nsISHEntry* aDestTreeParent)
+ : cloneID(aCloneID),
+ cloneChildren(aCloneChildren),
+ replaceEntry(aReplaceEntry),
+ destTreeParent(aDestTreeParent) {}
+
+ uint32_t cloneID;
+ bool cloneChildren;
+ nsISHEntry* replaceEntry;
+ nsISHEntry* destTreeParent;
+ nsCOMPtr<nsISHEntry> resultEntry;
+};
+
+nsresult nsSHistory::CloneAndReplaceChild(nsISHEntry* aEntry,
+ BrowsingContext* aOwnerBC,
+ int32_t aChildIndex, void* aData) {
+ nsCOMPtr<nsISHEntry> dest;
+
+ CloneAndReplaceData* data = static_cast<CloneAndReplaceData*>(aData);
+ uint32_t cloneID = data->cloneID;
+ nsISHEntry* replaceEntry = data->replaceEntry;
+
+ if (!aEntry) {
+ if (data->destTreeParent) {
+ data->destTreeParent->AddChild(nullptr, aChildIndex);
+ }
+ return NS_OK;
+ }
+
+ uint32_t srcID = aEntry->GetID();
+
+ nsresult rv = NS_OK;
+ if (srcID == cloneID) {
+ // Replace the entry
+ dest = replaceEntry;
+ } else {
+ // Clone the SHEntry...
+ rv = aEntry->Clone(getter_AddRefs(dest));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ dest->SetIsSubFrame(true);
+
+ if (srcID != cloneID || data->cloneChildren) {
+ // Walk the children
+ CloneAndReplaceData childData(cloneID, replaceEntry, data->cloneChildren,
+ dest);
+ rv = nsSHistory::WalkHistoryEntries(aEntry, aOwnerBC, CloneAndReplaceChild,
+ &childData);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (srcID != cloneID && aOwnerBC) {
+ nsSHistory::HandleEntriesToSwapInDocShell(aOwnerBC, aEntry, dest);
+ }
+
+ if (data->destTreeParent) {
+ data->destTreeParent->AddChild(dest, aChildIndex);
+ }
+ data->resultEntry = dest;
+ return rv;
+}
+
+// static
+nsresult nsSHistory::CloneAndReplace(
+ nsISHEntry* aSrcEntry, BrowsingContext* aOwnerBC, uint32_t aCloneID,
+ nsISHEntry* aReplaceEntry, bool aCloneChildren, nsISHEntry** aDestEntry) {
+ NS_ENSURE_ARG_POINTER(aDestEntry);
+ NS_ENSURE_TRUE(aReplaceEntry, NS_ERROR_FAILURE);
+ CloneAndReplaceData data(aCloneID, aReplaceEntry, aCloneChildren, nullptr);
+ nsresult rv = CloneAndReplaceChild(aSrcEntry, aOwnerBC, 0, &data);
+ data.resultEntry.swap(*aDestEntry);
+ return rv;
+}
+
+// static
+void nsSHistory::WalkContiguousEntries(
+ nsISHEntry* aEntry, const std::function<void(nsISHEntry*)>& aCallback) {
+ MOZ_ASSERT(aEntry);
+
+ nsCOMPtr<nsISHistory> shistory = aEntry->GetShistory();
+ if (!shistory) {
+ // If there is no session history in the entry, it means this is not a root
+ // entry. So, we can return from here.
+ return;
+ }
+
+ int32_t index = shistory->GetIndexOfEntry(aEntry);
+ int32_t count = shistory->GetCount();
+
+ nsCOMPtr<nsIURI> targetURI = aEntry->GetURI();
+
+ // First, call the callback on the input entry.
+ aCallback(aEntry);
+
+ // Walk backward to find the entries that have the same origin as the
+ // input entry.
+ for (int32_t i = index - 1; i >= 0; i--) {
+ RefPtr<nsISHEntry> entry;
+ shistory->GetEntryAtIndex(i, getter_AddRefs(entry));
+ if (entry) {
+ nsCOMPtr<nsIURI> uri = entry->GetURI();
+ if (NS_FAILED(nsContentUtils::GetSecurityManager()->CheckSameOriginURI(
+ targetURI, uri, false, false))) {
+ break;
+ }
+
+ aCallback(entry);
+ }
+ }
+
+ // Then, Walk forward.
+ for (int32_t i = index + 1; i < count; i++) {
+ RefPtr<nsISHEntry> entry;
+ shistory->GetEntryAtIndex(i, getter_AddRefs(entry));
+ if (entry) {
+ nsCOMPtr<nsIURI> uri = entry->GetURI();
+ if (NS_FAILED(nsContentUtils::GetSecurityManager()->CheckSameOriginURI(
+ targetURI, uri, false, false))) {
+ break;
+ }
+
+ aCallback(entry);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::AddChildSHEntryHelper(nsISHEntry* aCloneRef, nsISHEntry* aNewEntry,
+ BrowsingContext* aRootBC,
+ bool aCloneChildren) {
+ MOZ_ASSERT(aRootBC->IsTop());
+
+ /* You are currently in the rootDocShell.
+ * You will get here when a subframe has a new url
+ * to load and you have walked up the tree all the
+ * way to the top to clone the current SHEntry hierarchy
+ * and replace the subframe where a new url was loaded with
+ * a new entry.
+ */
+ nsCOMPtr<nsISHEntry> child;
+ nsCOMPtr<nsISHEntry> currentHE;
+ int32_t index = mIndex;
+ if (index < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ GetEntryAtIndex(index, getter_AddRefs(currentHE));
+ NS_ENSURE_TRUE(currentHE, NS_ERROR_FAILURE);
+
+ nsresult rv = NS_OK;
+ uint32_t cloneID = aCloneRef->GetID();
+ rv = nsSHistory::CloneAndReplace(currentHE, aRootBC, cloneID, aNewEntry,
+ aCloneChildren, getter_AddRefs(child));
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = AddEntry(child, true);
+ if (NS_SUCCEEDED(rv)) {
+ child->SetDocshellID(aRootBC->GetHistoryID());
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsSHistory::SetChildHistoryEntry(nsISHEntry* aEntry,
+ BrowsingContext* aBC,
+ int32_t aEntryIndex, void* aData) {
+ SwapEntriesData* data = static_cast<SwapEntriesData*>(aData);
+ if (!aBC || aBC == data->ignoreBC) {
+ return NS_OK;
+ }
+
+ nsISHEntry* destTreeRoot = data->destTreeRoot;
+
+ nsCOMPtr<nsISHEntry> destEntry;
+
+ if (data->destTreeParent) {
+ // aEntry is a clone of some child of destTreeParent, but since the
+ // trees aren't necessarily in sync, we'll have to locate it.
+ // Note that we could set aShell's entry to null if we don't find a
+ // corresponding entry under destTreeParent.
+
+ uint32_t targetID = aEntry->GetID();
+
+ // First look at the given index, since this is the common case.
+ nsCOMPtr<nsISHEntry> entry;
+ data->destTreeParent->GetChildAt(aEntryIndex, getter_AddRefs(entry));
+ if (entry && entry->GetID() == targetID) {
+ destEntry.swap(entry);
+ } else {
+ int32_t childCount;
+ data->destTreeParent->GetChildCount(&childCount);
+ for (int32_t i = 0; i < childCount; ++i) {
+ data->destTreeParent->GetChildAt(i, getter_AddRefs(entry));
+ if (!entry) {
+ continue;
+ }
+
+ if (entry->GetID() == targetID) {
+ destEntry.swap(entry);
+ break;
+ }
+ }
+ }
+ } else {
+ destEntry = destTreeRoot;
+ }
+
+ nsSHistory::HandleEntriesToSwapInDocShell(aBC, aEntry, destEntry);
+ // Now handle the children of aEntry.
+ SwapEntriesData childData = {data->ignoreBC, destTreeRoot, destEntry};
+ return nsSHistory::WalkHistoryEntries(aEntry, aBC, SetChildHistoryEntry,
+ &childData);
+}
+
+// static
+void nsSHistory::HandleEntriesToSwapInDocShell(
+ mozilla::dom::BrowsingContext* aBC, nsISHEntry* aOldEntry,
+ nsISHEntry* aNewEntry) {
+ bool shPref = mozilla::SessionHistoryInParent();
+ if (aBC->IsInProcess() || !shPref) {
+ nsDocShell* docshell = static_cast<nsDocShell*>(aBC->GetDocShell());
+ if (docshell) {
+ docshell->SwapHistoryEntries(aOldEntry, aNewEntry);
+ }
+ } else {
+ // FIXME Bug 1633988: Need to update entries?
+ }
+
+ // XXX Simplify this once the old and new session history implementations
+ // don't run at the same time.
+ if (shPref && XRE_IsParentProcess()) {
+ aBC->Canonical()->SwapHistoryEntries(aOldEntry, aNewEntry);
+ }
+}
+
+void nsSHistory::UpdateRootBrowsingContextState(BrowsingContext* aRootBC) {
+ if (aRootBC && aRootBC->EverAttached()) {
+ bool sameDocument = IsEmptyOrHasEntriesForSingleTopLevelPage();
+ if (sameDocument != aRootBC->GetIsSingleToplevelInHistory()) {
+ // If the browsing context is discarded then its session history is
+ // invalid and will go away.
+ Unused << aRootBC->SetIsSingleToplevelInHistory(sameDocument);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::AddToRootSessionHistory(bool aCloneChildren, nsISHEntry* aOSHE,
+ BrowsingContext* aRootBC,
+ nsISHEntry* aEntry, uint32_t aLoadType,
+ bool aShouldPersist,
+ Maybe<int32_t>* aPreviousEntryIndex,
+ Maybe<int32_t>* aLoadedEntryIndex) {
+ MOZ_ASSERT(aRootBC->IsTop());
+
+ nsresult rv = NS_OK;
+
+ // If we need to clone our children onto the new session
+ // history entry, do so now.
+ if (aCloneChildren && aOSHE) {
+ uint32_t cloneID = aOSHE->GetID();
+ nsCOMPtr<nsISHEntry> newEntry;
+ nsSHistory::CloneAndReplace(aOSHE, aRootBC, cloneID, aEntry, true,
+ getter_AddRefs(newEntry));
+ NS_ASSERTION(aEntry == newEntry,
+ "The new session history should be in the new entry");
+ }
+ // This is the root docshell
+ bool addToSHistory = !LOAD_TYPE_HAS_FLAGS(
+ aLoadType, nsIWebNavigation::LOAD_FLAGS_REPLACE_HISTORY);
+ if (!addToSHistory) {
+ // Replace current entry in session history; If the requested index is
+ // valid, it indicates the loading was triggered by a history load, and
+ // we should replace the entry at requested index instead.
+ int32_t index = GetIndexForReplace();
+
+ // Replace the current entry with the new entry
+ if (index >= 0) {
+ rv = ReplaceEntry(index, aEntry);
+ } else {
+ // If we're trying to replace an inexistant shistory entry, append.
+ addToSHistory = true;
+ }
+ }
+ if (addToSHistory) {
+ // Add to session history
+ *aPreviousEntryIndex = Some(mIndex);
+ rv = AddEntry(aEntry, aShouldPersist);
+ *aLoadedEntryIndex = Some(mIndex);
+ MOZ_LOG(gPageCacheLog, LogLevel::Verbose,
+ ("Previous index: %d, Loaded index: %d",
+ aPreviousEntryIndex->value(), aLoadedEntryIndex->value()));
+ }
+ if (NS_SUCCEEDED(rv)) {
+ aEntry->SetDocshellID(aRootBC->GetHistoryID());
+ }
+ return rv;
+}
+
+/* Add an entry to the History list at mIndex and
+ * increment the index to point to the new entry
+ */
+NS_IMETHODIMP
+nsSHistory::AddEntry(nsISHEntry* aSHEntry, bool aPersist) {
+ NS_ENSURE_ARG(aSHEntry);
+
+ nsCOMPtr<nsISHistory> shistoryOfEntry = aSHEntry->GetShistory();
+ if (shistoryOfEntry && shistoryOfEntry != this) {
+ NS_WARNING(
+ "The entry has been associated to another nsISHistory instance. "
+ "Try nsISHEntry.clone() and nsISHEntry.abandonBFCacheEntry() "
+ "first if you're copying an entry from another nsISHistory.");
+ return NS_ERROR_FAILURE;
+ }
+
+ aSHEntry->SetShistory(this);
+
+ // If we have a root docshell, update the docshell id of the root shentry to
+ // match the id of that docshell
+ RefPtr<BrowsingContext> rootBC = GetBrowsingContext();
+ if (rootBC) {
+ aSHEntry->SetDocshellID(mRootDocShellID);
+ }
+
+ if (mIndex >= 0) {
+ MOZ_ASSERT(mIndex < Length(), "Index out of range!");
+ if (mIndex >= Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mEntries[mIndex] && !mEntries[mIndex]->GetPersist()) {
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); });
+ aSHEntry->SetPersist(aPersist);
+ mEntries[mIndex] = aSHEntry;
+ UpdateRootBrowsingContextState();
+ return NS_OK;
+ }
+ }
+ SHistoryChangeNotifier change(this);
+
+ int32_t truncating = Length() - 1 - mIndex;
+ if (truncating > 0) {
+ NotifyListeners(mListeners,
+ [truncating](auto l) { l->OnHistoryTruncate(truncating); });
+ }
+
+ nsCOMPtr<nsIURI> uri = aSHEntry->GetURI();
+ NotifyListeners(mListeners,
+ [&uri, this](auto l) { l->OnHistoryNewEntry(uri, mIndex); });
+
+ // Remove all entries after the current one, add the new one, and set the
+ // new one as the current one.
+ MOZ_ASSERT(mIndex >= -1);
+ aSHEntry->SetPersist(aPersist);
+ mEntries.TruncateLength(mIndex + 1);
+ mEntries.AppendElement(aSHEntry);
+ mIndex++;
+ if (mIndex > 0) {
+ UpdateEntryLength(mEntries[mIndex - 1], mEntries[mIndex], false);
+ }
+
+ // Purge History list if it is too long
+ if (gHistoryMaxSize >= 0 && Length() > gHistoryMaxSize) {
+ PurgeHistory(Length() - gHistoryMaxSize);
+ }
+
+ UpdateRootBrowsingContextState();
+
+ return NS_OK;
+}
+
+void nsSHistory::NotifyOnHistoryReplaceEntry() {
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); });
+}
+
+/* Get size of the history list */
+NS_IMETHODIMP
+nsSHistory::GetCount(int32_t* aResult) {
+ MOZ_ASSERT(aResult, "null out param?");
+ *aResult = Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::GetIndex(int32_t* aResult) {
+ MOZ_ASSERT(aResult, "null out param?");
+ *aResult = mIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::SetIndex(int32_t aIndex) {
+ if (aIndex < 0 || aIndex >= Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mIndex = aIndex;
+ return NS_OK;
+}
+
+/* Get the requestedIndex */
+NS_IMETHODIMP
+nsSHistory::GetRequestedIndex(int32_t* aResult) {
+ MOZ_ASSERT(aResult, "null out param?");
+ *aResult = mRequestedIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::InternalSetRequestedIndex(int32_t aRequestedIndex) {
+ MOZ_ASSERT(aRequestedIndex >= -1 && aRequestedIndex < Length());
+ mRequestedIndex = aRequestedIndex;
+}
+
+NS_IMETHODIMP
+nsSHistory::GetEntryAtIndex(int32_t aIndex, nsISHEntry** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (aIndex < 0 || aIndex >= Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aResult = mEntries[aIndex];
+ NS_ADDREF(*aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(int32_t)
+nsSHistory::GetIndexOfEntry(nsISHEntry* aSHEntry) {
+ for (int32_t i = 0; i < Length(); i++) {
+ if (aSHEntry == mEntries[i]) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+static void LogEntry(nsISHEntry* aEntry, int32_t aIndex, int32_t aTotal,
+ const nsCString& aPrefix, bool aIsCurrent) {
+ if (!aEntry) {
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ (" %s+- %i SH Entry null\n", aPrefix.get(), aIndex));
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri = aEntry->GetURI();
+ nsAutoString title, name;
+ aEntry->GetTitle(title);
+ aEntry->GetName(name);
+
+ SHEntrySharedParentState* shared;
+ if (mozilla::SessionHistoryInParent()) {
+ shared = static_cast<SessionHistoryEntry*>(aEntry)->SharedInfo();
+ } else {
+ shared = static_cast<nsSHEntry*>(aEntry)->GetState();
+ }
+
+ nsID docShellId;
+ aEntry->GetDocshellID(docShellId);
+
+ int32_t childCount = aEntry->GetChildCount();
+
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ ("%s%s+- %i SH Entry %p %" PRIu64 " %s\n", aIsCurrent ? ">" : " ",
+ aPrefix.get(), aIndex, aEntry, shared->GetId(),
+ nsIDToCString(docShellId).get()));
+
+ nsCString prefix(aPrefix);
+ if (aIndex < aTotal - 1) {
+ prefix.AppendLiteral("| ");
+ } else {
+ prefix.AppendLiteral(" ");
+ }
+
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ (" %s%s URL = %s\n", prefix.get(), childCount > 0 ? "|" : " ",
+ uri->GetSpecOrDefault().get()));
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ (" %s%s Title = %s\n", prefix.get(), childCount > 0 ? "|" : " ",
+ NS_LossyConvertUTF16toASCII(title).get()));
+ MOZ_LOG(gSHLog, LogLevel::Debug,
+ (" %s%s Name = %s\n", prefix.get(), childCount > 0 ? "|" : " ",
+ NS_LossyConvertUTF16toASCII(name).get()));
+ MOZ_LOG(
+ gSHLog, LogLevel::Debug,
+ (" %s%s Is in BFCache = %s\n", prefix.get(), childCount > 0 ? "|" : " ",
+ aEntry->GetIsInBFCache() ? "true" : "false"));
+
+ nsCOMPtr<nsISHEntry> prevChild;
+ for (int32_t i = 0; i < childCount; ++i) {
+ nsCOMPtr<nsISHEntry> child;
+ aEntry->GetChildAt(i, getter_AddRefs(child));
+ LogEntry(child, i, childCount, prefix, false);
+ child.swap(prevChild);
+ }
+}
+
+void nsSHistory::LogHistory() {
+ if (!MOZ_LOG_TEST(gSHLog, LogLevel::Debug)) {
+ return;
+ }
+
+ MOZ_LOG(gSHLog, LogLevel::Debug, ("nsSHistory %p\n", this));
+ int32_t length = Length();
+ for (int32_t i = 0; i < length; i++) {
+ LogEntry(mEntries[i], i, length, EmptyCString(), i == mIndex);
+ }
+}
+
+void nsSHistory::WindowIndices(int32_t aIndex, int32_t* aOutStartIndex,
+ int32_t* aOutEndIndex) {
+ *aOutStartIndex = std::max(0, aIndex - nsSHistory::VIEWER_WINDOW);
+ *aOutEndIndex = std::min(Length() - 1, aIndex + nsSHistory::VIEWER_WINDOW);
+}
+
+static void MarkAsInitialEntry(
+ SessionHistoryEntry* aEntry,
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable) {
+ if (!aEntry->BCHistoryLength().Modified()) {
+ ++(aEntry->BCHistoryLength());
+ }
+ aHashtable.InsertOrUpdate(aEntry->DocshellID(), aEntry);
+ for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) {
+ if (entry) {
+ MarkAsInitialEntry(entry, aHashtable);
+ }
+ }
+}
+
+static void ClearEntries(SessionHistoryEntry* aEntry) {
+ aEntry->ClearBCHistoryLength();
+ for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) {
+ if (entry) {
+ ClearEntries(entry);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::PurgeHistory(int32_t aNumEntries) {
+ if (Length() <= 0 || aNumEntries <= 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SHistoryChangeNotifier change(this);
+
+ aNumEntries = std::min(aNumEntries, Length());
+
+ NotifyListeners(mListeners,
+ [aNumEntries](auto l) { l->OnHistoryPurge(aNumEntries); });
+
+ // Set all the entries hanging of the first entry that we keep
+ // (mEntries[aNumEntries]) as being created as the result of a load
+ // (so contributing one to their BCHistoryLength).
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*> docshellIDToEntry;
+ if (aNumEntries != Length()) {
+ nsCOMPtr<SessionHistoryEntry> she =
+ do_QueryInterface(mEntries[aNumEntries]);
+ if (she) {
+ MarkAsInitialEntry(she, docshellIDToEntry);
+ }
+ }
+
+ // Reset the BCHistoryLength of all the entries that we're removing to a new
+ // counter with value 0 while decreasing their contribution to a shared
+ // BCHistoryLength. The end result is that they don't contribute to the
+ // BCHistoryLength of any other entry anymore.
+ for (int32_t i = 0; i < aNumEntries; ++i) {
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(mEntries[i]);
+ if (she) {
+ ClearEntries(she);
+ }
+ }
+
+ RefPtr<BrowsingContext> rootBC = GetBrowsingContext();
+ if (rootBC) {
+ rootBC->PreOrderWalk([&docshellIDToEntry](BrowsingContext* aBC) {
+ SessionHistoryEntry* entry = docshellIDToEntry.Get(aBC->GetHistoryID());
+ Unused << aBC->SetHistoryEntryCount(
+ entry ? uint32_t(entry->BCHistoryLength()) : 0);
+ });
+ }
+
+ // Remove the first `aNumEntries` entries.
+ mEntries.RemoveElementsAt(0, aNumEntries);
+
+ // Adjust the indices, but don't let them go below -1.
+ mIndex -= aNumEntries;
+ mIndex = std::max(mIndex, -1);
+ mRequestedIndex -= aNumEntries;
+ mRequestedIndex = std::max(mRequestedIndex, -1);
+
+ if (rootBC && rootBC->GetDocShell()) {
+ rootBC->GetDocShell()->HistoryPurged(aNumEntries);
+ }
+
+ UpdateRootBrowsingContextState(rootBC);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::AddSHistoryListener(nsISHistoryListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ // Check if the listener supports Weak Reference. This is a must.
+ // This listener functionality is used by embedders and we want to
+ // have the right ownership with who ever listens to SHistory
+ nsWeakPtr listener = do_GetWeakReference(aListener);
+ if (!listener) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mListeners.AppendElementUnlessExists(listener);
+ return NS_OK;
+}
+
+void nsSHistory::NotifyListenersContentViewerEvicted(uint32_t aNumEvicted) {
+ NotifyListeners(mListeners, [aNumEvicted](auto l) {
+ l->OnContentViewerEvicted(aNumEvicted);
+ });
+}
+
+NS_IMETHODIMP
+nsSHistory::RemoveSHistoryListener(nsISHistoryListener* aListener) {
+ // Make sure the listener that wants to be removed is the
+ // one we have in store.
+ nsWeakPtr listener = do_GetWeakReference(aListener);
+ mListeners.RemoveElement(listener);
+ return NS_OK;
+}
+
+/* Replace an entry in the History list at a particular index.
+ * Do not update index or count.
+ */
+NS_IMETHODIMP
+nsSHistory::ReplaceEntry(int32_t aIndex, nsISHEntry* aReplaceEntry) {
+ NS_ENSURE_ARG(aReplaceEntry);
+
+ if (aIndex < 0 || aIndex >= Length()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsISHistory> shistoryOfEntry = aReplaceEntry->GetShistory();
+ if (shistoryOfEntry && shistoryOfEntry != this) {
+ NS_WARNING(
+ "The entry has been associated to another nsISHistory instance. "
+ "Try nsISHEntry.clone() and nsISHEntry.abandonBFCacheEntry() "
+ "first if you're copying an entry from another nsISHistory.");
+ return NS_ERROR_FAILURE;
+ }
+
+ aReplaceEntry->SetShistory(this);
+
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryReplaceEntry(); });
+
+ aReplaceEntry->SetPersist(true);
+ mEntries[aIndex] = aReplaceEntry;
+
+ UpdateRootBrowsingContextState();
+
+ return NS_OK;
+}
+
+// Calls OnHistoryReload on all registered session history listeners.
+// Listeners may return 'false' to cancel an action so make sure that we
+// set the return value to 'false' if one of the listeners wants to cancel.
+NS_IMETHODIMP
+nsSHistory::NotifyOnHistoryReload(bool* aCanReload) {
+ *aCanReload = true;
+
+ for (const nsWeakPtr& weakPtr : mListeners.EndLimitedRange()) {
+ nsCOMPtr<nsISHistoryListener> listener = do_QueryReferent(weakPtr);
+ if (listener) {
+ bool retval = true;
+
+ if (NS_SUCCEEDED(listener->OnHistoryReload(&retval)) && !retval) {
+ *aCanReload = false;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::EvictOutOfRangeContentViewers(int32_t aIndex) {
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ ("nsSHistory::EvictOutOfRangeContentViewers %i", aIndex));
+
+ // Check our per SHistory object limit in the currently navigated SHistory
+ EvictOutOfRangeWindowContentViewers(aIndex);
+ // Check our total limit across all SHistory objects
+ GloballyEvictContentViewers();
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::EvictContentViewersOrReplaceEntry(nsISHEntry* aNewSHEntry,
+ bool aReplace) {
+ if (!aReplace) {
+ int32_t curIndex;
+ GetIndex(&curIndex);
+ if (curIndex > -1) {
+ EvictOutOfRangeContentViewers(curIndex);
+ }
+ } else {
+ nsCOMPtr<nsISHEntry> rootSHEntry = nsSHistory::GetRootSHEntry(aNewSHEntry);
+
+ int32_t index = GetIndexOfEntry(rootSHEntry);
+ if (index > -1) {
+ ReplaceEntry(index, rootSHEntry);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::EvictAllContentViewers() {
+ // XXXbz we don't actually do a good job of evicting things as we should, so
+ // we might have viewers quite far from mIndex. So just evict everything.
+ for (int32_t i = 0; i < Length(); i++) {
+ EvictContentViewerForEntry(mEntries[i]);
+ }
+
+ return NS_OK;
+}
+
+static void FinishRestore(CanonicalBrowsingContext* aBrowsingContext,
+ nsDocShellLoadState* aLoadState,
+ SessionHistoryEntry* aEntry,
+ nsFrameLoader* aFrameLoader, bool aCanSave) {
+ MOZ_ASSERT(aEntry);
+ MOZ_ASSERT(aFrameLoader);
+
+ aEntry->SetFrameLoader(nullptr);
+
+ nsCOMPtr<nsISHistory> shistory = aEntry->GetShistory();
+ int32_t indexOfHistoryLoad =
+ shistory ? shistory->GetIndexOfEntry(aEntry) : -1;
+
+ nsCOMPtr<nsFrameLoaderOwner> frameLoaderOwner =
+ do_QueryInterface(aBrowsingContext->GetEmbedderElement());
+ if (frameLoaderOwner && aFrameLoader->GetMaybePendingBrowsingContext() &&
+ indexOfHistoryLoad >= 0) {
+ RefPtr<BrowsingContextWebProgress> webProgress =
+ aBrowsingContext->GetWebProgress();
+ if (webProgress) {
+ // Synthesize a STATE_START WebProgress state change event from here
+ // in order to ensure emitting it on the BrowsingContext we navigate
+ // *from* instead of the BrowsingContext we navigate *to*. This will fire
+ // before and the next one will be ignored by BrowsingContextWebProgress:
+ // https://searchfox.org/mozilla-central/rev/77f0b36028b2368e342c982ea47609040b399d89/docshell/base/BrowsingContextWebProgress.cpp#196-203
+ nsCOMPtr<nsIURI> nextURI = aEntry->GetURI();
+ nsCOMPtr<nsIURI> nextOriginalURI = aEntry->GetOriginalURI();
+ nsCOMPtr<nsIRequest> request = MakeAndAddRef<RemoteWebProgressRequest>(
+ nextURI, nextOriginalURI ? nextOriginalURI : nextURI,
+ ""_ns /* aMatchedList */);
+ webProgress->OnStateChange(webProgress, request,
+ nsIWebProgressListener::STATE_START |
+ nsIWebProgressListener::STATE_IS_DOCUMENT |
+ nsIWebProgressListener::STATE_IS_REQUEST |
+ nsIWebProgressListener::STATE_IS_WINDOW |
+ nsIWebProgressListener::STATE_IS_NETWORK,
+ NS_OK);
+ }
+
+ RefPtr<CanonicalBrowsingContext> loadingBC =
+ aFrameLoader->GetMaybePendingBrowsingContext()->Canonical();
+ RefPtr<nsFrameLoader> currentFrameLoader =
+ frameLoaderOwner->GetFrameLoader();
+ // The current page can be bfcached, store the
+ // nsFrameLoader in the current SessionHistoryEntry.
+ RefPtr<SessionHistoryEntry> currentSHEntry =
+ aBrowsingContext->GetActiveSessionHistoryEntry();
+ if (currentSHEntry) {
+ // Update layout history state now, before we change the IsInBFCache flag
+ // and the active session history entry.
+ aBrowsingContext->SynchronizeLayoutHistoryState();
+
+ if (aCanSave) {
+ currentSHEntry->SetFrameLoader(currentFrameLoader);
+ Unused << aBrowsingContext->SetIsInBFCache(true);
+ }
+ }
+
+ if (aBrowsingContext->IsActive()) {
+ loadingBC->PreOrderWalk([&](BrowsingContext* aContext) {
+ if (BrowserParent* bp = aContext->Canonical()->GetBrowserParent()) {
+ ProcessPriorityManager::BrowserPriorityChanged(bp, true);
+ }
+ });
+ }
+
+ if (aEntry) {
+ aEntry->SetWireframe(Nothing());
+ }
+
+ // ReplacedBy will swap the entry back.
+ aBrowsingContext->SetActiveSessionHistoryEntry(aEntry);
+ loadingBC->SetActiveSessionHistoryEntry(nullptr);
+ NavigationIsolationOptions options;
+ aBrowsingContext->ReplacedBy(loadingBC, options);
+
+ // Assuming we still have the session history, update the index.
+ if (loadingBC->GetSessionHistory()) {
+ shistory->InternalSetRequestedIndex(indexOfHistoryLoad);
+ shistory->UpdateIndex();
+ }
+ loadingBC->HistoryCommitIndexAndLength();
+
+ // ResetSHEntryHasUserInteractionCache(); ?
+ // browser.navigation.requireUserInteraction is still
+ // disabled everywhere.
+
+ frameLoaderOwner->RestoreFrameLoaderFromBFCache(aFrameLoader);
+ // EvictOutOfRangeContentViewers is called here explicitly to
+ // possibly evict the now in the bfcache document.
+ // HistoryCommitIndexAndLength might not have evicted that before the
+ // FrameLoader swap.
+ shistory->EvictOutOfRangeContentViewers(indexOfHistoryLoad);
+
+ // The old page can't be stored in the bfcache,
+ // destroy the nsFrameLoader.
+ if (!aCanSave && currentFrameLoader) {
+ currentFrameLoader->Destroy();
+ }
+
+ Unused << loadingBC->SetIsInBFCache(false);
+
+ // We need to call this after we've restored the page from BFCache (see
+ // SetIsInBFCache(false) above), so that the page is not frozen anymore and
+ // the right focus events are fired.
+ frameLoaderOwner->UpdateFocusAndMouseEnterStateAfterFrameLoaderChange();
+
+ return;
+ }
+
+ aFrameLoader->Destroy();
+
+ // Fall back to do a normal load.
+ aBrowsingContext->LoadURI(aLoadState, false);
+}
+
+/* static */
+void nsSHistory::LoadURIOrBFCache(LoadEntryResult& aLoadEntry) {
+ if (mozilla::BFCacheInParent() && aLoadEntry.mBrowsingContext->IsTop()) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ RefPtr<nsDocShellLoadState> loadState = aLoadEntry.mLoadState;
+ RefPtr<CanonicalBrowsingContext> canonicalBC =
+ aLoadEntry.mBrowsingContext->Canonical();
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(loadState->SHEntry());
+ nsCOMPtr<SessionHistoryEntry> currentShe =
+ canonicalBC->GetActiveSessionHistoryEntry();
+ MOZ_ASSERT(she);
+ RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader();
+ if (frameLoader &&
+ (!currentShe || (she->SharedInfo() != currentShe->SharedInfo() &&
+ !currentShe->GetFrameLoader()))) {
+ bool canSave = (!currentShe || currentShe->GetSaveLayoutStateFlag()) &&
+ canonicalBC->AllowedInBFCache(Nothing(), nullptr);
+
+ MOZ_LOG(gSHIPBFCacheLog, LogLevel::Debug,
+ ("nsSHistory::LoadURIOrBFCache "
+ "saving presentation=%i",
+ canSave));
+
+ nsCOMPtr<nsFrameLoaderOwner> frameLoaderOwner =
+ do_QueryInterface(canonicalBC->GetEmbedderElement());
+ if (frameLoaderOwner) {
+ RefPtr<nsFrameLoader> currentFrameLoader =
+ frameLoaderOwner->GetFrameLoader();
+ if (currentFrameLoader &&
+ currentFrameLoader->GetMaybePendingBrowsingContext()) {
+ if (WindowGlobalParent* wgp =
+ currentFrameLoader->GetMaybePendingBrowsingContext()
+ ->Canonical()
+ ->GetCurrentWindowGlobal()) {
+ wgp->PermitUnload([canonicalBC, loadState, she, frameLoader,
+ currentFrameLoader, canSave](bool aAllow) {
+ if (aAllow && !canonicalBC->IsReplaced()) {
+ FinishRestore(canonicalBC, loadState, she, frameLoader,
+ canSave && canonicalBC->AllowedInBFCache(
+ Nothing(), nullptr));
+ } else if (currentFrameLoader->GetMaybePendingBrowsingContext()) {
+ nsISHistory* shistory =
+ currentFrameLoader->GetMaybePendingBrowsingContext()
+ ->Canonical()
+ ->GetSessionHistory();
+ if (shistory) {
+ shistory->InternalSetRequestedIndex(-1);
+ }
+ }
+ });
+ return;
+ }
+ }
+ }
+
+ FinishRestore(canonicalBC, loadState, she, frameLoader, canSave);
+ return;
+ }
+ if (frameLoader) {
+ she->SetFrameLoader(nullptr);
+ frameLoader->Destroy();
+ }
+ }
+
+ RefPtr<BrowsingContext> bc = aLoadEntry.mBrowsingContext;
+ RefPtr<nsDocShellLoadState> loadState = aLoadEntry.mLoadState;
+ bc->LoadURI(loadState, false);
+}
+
+/* static */
+void nsSHistory::LoadURIs(nsTArray<LoadEntryResult>& aLoadResults) {
+ for (LoadEntryResult& loadEntry : aLoadResults) {
+ LoadURIOrBFCache(loadEntry);
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::Reload(uint32_t aReloadFlags) {
+ nsTArray<LoadEntryResult> loadResults;
+ nsresult rv = Reload(aReloadFlags, loadResults);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (loadResults.IsEmpty()) {
+ return NS_OK;
+ }
+
+ LoadURIs(loadResults);
+ return NS_OK;
+}
+
+nsresult nsSHistory::Reload(uint32_t aReloadFlags,
+ nsTArray<LoadEntryResult>& aLoadResults) {
+ MOZ_ASSERT(aLoadResults.IsEmpty());
+
+ uint32_t loadType;
+ if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY &&
+ aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE) {
+ loadType = LOAD_RELOAD_BYPASS_PROXY_AND_CACHE;
+ } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_PROXY) {
+ loadType = LOAD_RELOAD_BYPASS_PROXY;
+ } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_BYPASS_CACHE) {
+ loadType = LOAD_RELOAD_BYPASS_CACHE;
+ } else if (aReloadFlags & nsIWebNavigation::LOAD_FLAGS_CHARSET_CHANGE) {
+ loadType = LOAD_RELOAD_CHARSET_CHANGE;
+ } else {
+ loadType = LOAD_RELOAD_NORMAL;
+ }
+
+ // We are reloading. Send Reload notifications.
+ // nsDocShellLoadFlagType is not public, where as nsIWebNavigation
+ // is public. So send the reload notifications with the
+ // nsIWebNavigation flags.
+ bool canNavigate = true;
+ MOZ_ALWAYS_SUCCEEDS(NotifyOnHistoryReload(&canNavigate));
+ if (!canNavigate) {
+ return NS_OK;
+ }
+
+ nsresult rv = LoadEntry(
+ mIndex, loadType, HIST_CMD_RELOAD, aLoadResults, /* aSameEpoch */ false,
+ /* aLoadCurrentEntry */ true,
+ aReloadFlags & nsIWebNavigation::LOAD_FLAGS_USER_ACTIVATION);
+ if (NS_FAILED(rv)) {
+ aLoadResults.Clear();
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::ReloadCurrentEntry() {
+ nsTArray<LoadEntryResult> loadResults;
+ nsresult rv = ReloadCurrentEntry(loadResults);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LoadURIs(loadResults);
+ return NS_OK;
+}
+
+nsresult nsSHistory::ReloadCurrentEntry(
+ nsTArray<LoadEntryResult>& aLoadResults) {
+ // Notify listeners
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryGotoIndex(); });
+
+ return LoadEntry(mIndex, LOAD_HISTORY, HIST_CMD_RELOAD, aLoadResults,
+ /* aSameEpoch */ false, /* aLoadCurrentEntry */ true,
+ /* aUserActivation */ false);
+}
+
+void nsSHistory::EvictOutOfRangeWindowContentViewers(int32_t aIndex) {
+ // XXX rename method to EvictContentViewersExceptAroundIndex, or something.
+
+ // We need to release all content viewers that are no longer in the range
+ //
+ // aIndex - VIEWER_WINDOW to aIndex + VIEWER_WINDOW
+ //
+ // to ensure that this SHistory object isn't responsible for more than
+ // VIEWER_WINDOW content viewers. But our job is complicated by the
+ // fact that two entries which are related by either hash navigations or
+ // history.pushState will have the same content viewer.
+ //
+ // To illustrate the issue, suppose VIEWER_WINDOW = 3 and we have four
+ // linked entries in our history. Suppose we then add a new content
+ // viewer and call into this function. So the history looks like:
+ //
+ // A A A A B
+ // + *
+ //
+ // where the letters are content viewers and + and * denote the beginning and
+ // end of the range aIndex +/- VIEWER_WINDOW.
+ //
+ // Although one copy of the content viewer A exists outside the range, we
+ // don't want to evict A, because it has other copies in range!
+ //
+ // We therefore adjust our eviction strategy to read:
+ //
+ // Evict each content viewer outside the range aIndex -/+
+ // VIEWER_WINDOW, unless that content viewer also appears within the
+ // range.
+ //
+ // (Note that it's entirely legal to have two copies of one content viewer
+ // separated by a different content viewer -- call pushState twice, go back
+ // once, and refresh -- so we can't rely on identical viewers only appearing
+ // adjacent to one another.)
+
+ if (aIndex < 0) {
+ return;
+ }
+ NS_ENSURE_TRUE_VOID(aIndex < Length());
+
+ // Calculate the range that's safe from eviction.
+ int32_t startSafeIndex, endSafeIndex;
+ WindowIndices(aIndex, &startSafeIndex, &endSafeIndex);
+
+ LOG(
+ ("EvictOutOfRangeWindowContentViewers(index=%d), "
+ "Length()=%d. Safe range [%d, %d]",
+ aIndex, Length(), startSafeIndex, endSafeIndex));
+
+ // The content viewers in range aIndex -/+ VIEWER_WINDOW will not be
+ // evicted. Collect a set of them so we don't accidentally evict one of them
+ // if it appears outside this range.
+ nsCOMArray<nsIContentViewer> safeViewers;
+ nsTArray<RefPtr<nsFrameLoader>> safeFrameLoaders;
+ for (int32_t i = startSafeIndex; i <= endSafeIndex; i++) {
+ nsCOMPtr<nsIContentViewer> viewer = mEntries[i]->GetContentViewer();
+ if (viewer) {
+ safeViewers.AppendObject(viewer);
+ } else if (nsCOMPtr<SessionHistoryEntry> she =
+ do_QueryInterface(mEntries[i])) {
+ nsFrameLoader* frameLoader = she->GetFrameLoader();
+ if (frameLoader) {
+ safeFrameLoaders.AppendElement(frameLoader);
+ }
+ }
+ }
+
+ // Walk the SHistory list and evict any content viewers that aren't safe.
+ // (It's important that the condition checks Length(), rather than a cached
+ // copy of Length(), because the length might change between iterations.)
+ for (int32_t i = 0; i < Length(); i++) {
+ nsCOMPtr<nsISHEntry> entry = mEntries[i];
+ nsCOMPtr<nsIContentViewer> viewer = entry->GetContentViewer();
+ if (viewer) {
+ if (safeViewers.IndexOf(viewer) == -1) {
+ EvictContentViewerForEntry(entry);
+ }
+ } else if (nsCOMPtr<SessionHistoryEntry> she =
+ do_QueryInterface(mEntries[i])) {
+ nsFrameLoader* frameLoader = she->GetFrameLoader();
+ if (frameLoader) {
+ if (!safeFrameLoaders.Contains(frameLoader)) {
+ EvictContentViewerForEntry(entry);
+ }
+ }
+ }
+ }
+}
+
+namespace {
+
+class EntryAndDistance {
+ public:
+ EntryAndDistance(nsSHistory* aSHistory, nsISHEntry* aEntry, uint32_t aDist)
+ : mSHistory(aSHistory),
+ mEntry(aEntry),
+ mViewer(aEntry->GetContentViewer()),
+ mLastTouched(mEntry->GetLastTouched()),
+ mDistance(aDist) {
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aEntry);
+ if (she) {
+ mFrameLoader = she->GetFrameLoader();
+ }
+ NS_ASSERTION(mViewer || mFrameLoader,
+ "Entry should have a content viewer or frame loader.");
+ }
+
+ bool operator<(const EntryAndDistance& aOther) const {
+ // Compare distances first, and fall back to last-accessed times.
+ if (aOther.mDistance != this->mDistance) {
+ return this->mDistance < aOther.mDistance;
+ }
+
+ return this->mLastTouched < aOther.mLastTouched;
+ }
+
+ bool operator==(const EntryAndDistance& aOther) const {
+ // This is a little silly; we need == so the default comaprator can be
+ // instantiated, but this function is never actually called when we sort
+ // the list of EntryAndDistance objects.
+ return aOther.mDistance == this->mDistance &&
+ aOther.mLastTouched == this->mLastTouched;
+ }
+
+ RefPtr<nsSHistory> mSHistory;
+ nsCOMPtr<nsISHEntry> mEntry;
+ nsCOMPtr<nsIContentViewer> mViewer;
+ RefPtr<nsFrameLoader> mFrameLoader;
+ uint32_t mLastTouched;
+ int32_t mDistance;
+};
+
+} // namespace
+
+// static
+void nsSHistory::GloballyEvictContentViewers() {
+ // First, collect from each SHistory object the entries which have a cached
+ // content viewer. Associate with each entry its distance from its SHistory's
+ // current index.
+
+ nsTArray<EntryAndDistance> entries;
+
+ for (auto shist : gSHistoryList.mList) {
+ // Maintain a list of the entries which have viewers and belong to
+ // this particular shist object. We'll add this list to the global list,
+ // |entries|, eventually.
+ nsTArray<EntryAndDistance> shEntries;
+
+ // Content viewers are likely to exist only within shist->mIndex -/+
+ // VIEWER_WINDOW, so only search within that range.
+ //
+ // A content viewer might exist outside that range due to either:
+ //
+ // * history.pushState or hash navigations, in which case a copy of the
+ // content viewer should exist within the range, or
+ //
+ // * bugs which cause us not to call nsSHistory::EvictContentViewers()
+ // often enough. Once we do call EvictContentViewers() for the
+ // SHistory object in question, we'll do a full search of its history
+ // and evict the out-of-range content viewers, so we don't bother here.
+ //
+ int32_t startIndex, endIndex;
+ shist->WindowIndices(shist->mIndex, &startIndex, &endIndex);
+ for (int32_t i = startIndex; i <= endIndex; i++) {
+ nsCOMPtr<nsISHEntry> entry = shist->mEntries[i];
+ nsCOMPtr<nsIContentViewer> contentViewer = entry->GetContentViewer();
+
+ bool found = false;
+ bool hasContentViewerOrFrameLoader = false;
+ if (contentViewer) {
+ hasContentViewerOrFrameLoader = true;
+ // Because one content viewer might belong to multiple SHEntries, we
+ // have to search through shEntries to see if we already know
+ // about this content viewer. If we find the viewer, update its
+ // distance from the SHistory's index and continue.
+ for (uint32_t j = 0; j < shEntries.Length(); j++) {
+ EntryAndDistance& container = shEntries[j];
+ if (container.mViewer == contentViewer) {
+ container.mDistance =
+ std::min(container.mDistance, DeprecatedAbs(i - shist->mIndex));
+ found = true;
+ break;
+ }
+ }
+ } else if (nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(entry)) {
+ if (RefPtr<nsFrameLoader> frameLoader = she->GetFrameLoader()) {
+ hasContentViewerOrFrameLoader = true;
+ // Similar search as above but using frameloader.
+ for (uint32_t j = 0; j < shEntries.Length(); j++) {
+ EntryAndDistance& container = shEntries[j];
+ if (container.mFrameLoader == frameLoader) {
+ container.mDistance = std::min(container.mDistance,
+ DeprecatedAbs(i - shist->mIndex));
+ found = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // If we didn't find a EntryAndDistance for this content viewer /
+ // frameloader, make a new one.
+ if (hasContentViewerOrFrameLoader && !found) {
+ EntryAndDistance container(shist, entry,
+ DeprecatedAbs(i - shist->mIndex));
+ shEntries.AppendElement(container);
+ }
+ }
+
+ // We've found all the entries belonging to shist which have viewers.
+ // Add those entries to our global list and move on.
+ entries.AppendElements(shEntries);
+ }
+
+ // We now have collected all cached content viewers. First check that we
+ // have enough that we actually need to evict some.
+ if ((int32_t)entries.Length() <= sHistoryMaxTotalViewers) {
+ return;
+ }
+
+ // If we need to evict, sort our list of entries and evict the largest
+ // ones. (We could of course get better algorithmic complexity here by using
+ // a heap or something more clever. But sHistoryMaxTotalViewers isn't large,
+ // so let's not worry about it.)
+ entries.Sort();
+
+ for (int32_t i = entries.Length() - 1; i >= sHistoryMaxTotalViewers; --i) {
+ (entries[i].mSHistory)->EvictContentViewerForEntry(entries[i].mEntry);
+ }
+}
+
+nsresult nsSHistory::FindEntryForBFCache(SHEntrySharedParentState* aEntry,
+ nsISHEntry** aResult,
+ int32_t* aResultIndex) {
+ *aResult = nullptr;
+ *aResultIndex = -1;
+
+ int32_t startIndex, endIndex;
+ WindowIndices(mIndex, &startIndex, &endIndex);
+
+ for (int32_t i = startIndex; i <= endIndex; ++i) {
+ nsCOMPtr<nsISHEntry> shEntry = mEntries[i];
+
+ // Does shEntry have the same BFCacheEntry as the argument to this method?
+ if (shEntry->HasBFCacheEntry(aEntry)) {
+ shEntry.forget(aResult);
+ *aResultIndex = i;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::EvictExpiredContentViewerForEntry(
+ SHEntrySharedParentState* aEntry) {
+ int32_t index;
+ nsCOMPtr<nsISHEntry> shEntry;
+ FindEntryForBFCache(aEntry, getter_AddRefs(shEntry), &index);
+
+ if (index == mIndex) {
+ NS_WARNING("How did the current SHEntry expire?");
+ }
+
+ if (shEntry) {
+ EvictContentViewerForEntry(shEntry);
+ }
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::AddToExpirationTracker(SHEntrySharedParentState* aEntry) {
+ RefPtr<SHEntrySharedParentState> entry = aEntry;
+ if (!mHistoryTracker || !entry) {
+ return;
+ }
+
+ mHistoryTracker->AddObject(entry);
+ return;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::RemoveFromExpirationTracker(SHEntrySharedParentState* aEntry) {
+ RefPtr<SHEntrySharedParentState> entry = aEntry;
+ MOZ_ASSERT(mHistoryTracker && !mHistoryTracker->IsEmpty());
+ if (!mHistoryTracker || !entry) {
+ return;
+ }
+
+ mHistoryTracker->RemoveObject(entry);
+}
+
+// Evicts all content viewers in all history objects. This is very
+// inefficient, because it requires a linear search through all SHistory
+// objects for each viewer to be evicted. However, this method is called
+// infrequently -- only when the disk or memory cache is cleared.
+
+// static
+void nsSHistory::GloballyEvictAllContentViewers() {
+ int32_t maxViewers = sHistoryMaxTotalViewers;
+ sHistoryMaxTotalViewers = 0;
+ GloballyEvictContentViewers();
+ sHistoryMaxTotalViewers = maxViewers;
+}
+
+void GetDynamicChildren(nsISHEntry* aEntry, nsTArray<nsID>& aDocshellIDs) {
+ int32_t count = aEntry->GetChildCount();
+ for (int32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsISHEntry> child;
+ aEntry->GetChildAt(i, getter_AddRefs(child));
+ if (child) {
+ if (child->IsDynamicallyAdded()) {
+ child->GetDocshellID(*aDocshellIDs.AppendElement());
+ } else {
+ GetDynamicChildren(child, aDocshellIDs);
+ }
+ }
+ }
+}
+
+bool RemoveFromSessionHistoryEntry(nsISHEntry* aRoot,
+ nsTArray<nsID>& aDocshellIDs) {
+ bool didRemove = false;
+ int32_t childCount = aRoot->GetChildCount();
+ for (int32_t i = childCount - 1; i >= 0; --i) {
+ nsCOMPtr<nsISHEntry> child;
+ aRoot->GetChildAt(i, getter_AddRefs(child));
+ if (child) {
+ nsID docshelldID;
+ child->GetDocshellID(docshelldID);
+ if (aDocshellIDs.Contains(docshelldID)) {
+ didRemove = true;
+ aRoot->RemoveChild(child);
+ } else if (RemoveFromSessionHistoryEntry(child, aDocshellIDs)) {
+ didRemove = true;
+ }
+ }
+ }
+ return didRemove;
+}
+
+bool RemoveChildEntries(nsISHistory* aHistory, int32_t aIndex,
+ nsTArray<nsID>& aEntryIDs) {
+ nsCOMPtr<nsISHEntry> root;
+ aHistory->GetEntryAtIndex(aIndex, getter_AddRefs(root));
+ return root ? RemoveFromSessionHistoryEntry(root, aEntryIDs) : false;
+}
+
+bool IsSameTree(nsISHEntry* aEntry1, nsISHEntry* aEntry2) {
+ if (!aEntry1 && !aEntry2) {
+ return true;
+ }
+ if ((!aEntry1 && aEntry2) || (aEntry1 && !aEntry2)) {
+ return false;
+ }
+ uint32_t id1 = aEntry1->GetID();
+ uint32_t id2 = aEntry2->GetID();
+ if (id1 != id2) {
+ return false;
+ }
+
+ int32_t count1 = aEntry1->GetChildCount();
+ int32_t count2 = aEntry2->GetChildCount();
+ // We allow null entries in the end of the child list.
+ int32_t count = std::max(count1, count2);
+ for (int32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsISHEntry> child1, child2;
+ aEntry1->GetChildAt(i, getter_AddRefs(child1));
+ aEntry2->GetChildAt(i, getter_AddRefs(child2));
+ if (!IsSameTree(child1, child2)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool nsSHistory::RemoveDuplicate(int32_t aIndex, bool aKeepNext) {
+ NS_ASSERTION(aIndex >= 0, "aIndex must be >= 0!");
+ NS_ASSERTION(aIndex != 0 || aKeepNext,
+ "If we're removing index 0 we must be keeping the next");
+ NS_ASSERTION(aIndex != mIndex, "Shouldn't remove mIndex!");
+
+ int32_t compareIndex = aKeepNext ? aIndex + 1 : aIndex - 1;
+
+ nsresult rv;
+ nsCOMPtr<nsISHEntry> root1, root2;
+ rv = GetEntryAtIndex(aIndex, getter_AddRefs(root1));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ rv = GetEntryAtIndex(compareIndex, getter_AddRefs(root2));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ SHistoryChangeNotifier change(this);
+
+ if (IsSameTree(root1, root2)) {
+ if (aIndex < compareIndex) {
+ // If we're removing the entry with the lower index we need to move its
+ // BCHistoryLength to the entry we're keeping. If we're removing the entry
+ // with the higher index then it shouldn't have a modified
+ // BCHistoryLength.
+ UpdateEntryLength(root1, root2, true);
+ }
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(root1);
+ if (she) {
+ ClearEntries(she);
+ }
+ mEntries.RemoveElementAt(aIndex);
+
+ // FIXME Bug 1546350: Reimplement history listeners.
+ // if (mRootBC && mRootBC->GetDocShell()) {
+ // static_cast<nsDocShell*>(mRootBC->GetDocShell())
+ // ->HistoryEntryRemoved(aIndex);
+ //}
+
+ // Adjust our indices to reflect the removed entry.
+ if (mIndex > aIndex) {
+ mIndex = mIndex - 1;
+ }
+
+ // NB: If the entry we are removing is the entry currently
+ // being navigated to (mRequestedIndex) then we adjust the index
+ // only if we're not keeping the next entry (because if we are keeping
+ // the next entry (because the current is a duplicate of the next), then
+ // that entry slides into the spot that we're currently pointing to.
+ // We don't do this adjustment for mIndex because mIndex cannot equal
+ // aIndex.
+
+ // NB: We don't need to guard on mRequestedIndex being nonzero here,
+ // because either they're strictly greater than aIndex which is at least
+ // zero, or they are equal to aIndex in which case aKeepNext must be true
+ // if aIndex is zero.
+ if (mRequestedIndex > aIndex || (mRequestedIndex == aIndex && !aKeepNext)) {
+ mRequestedIndex = mRequestedIndex - 1;
+ }
+
+ return true;
+ }
+ return false;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex) {
+ bool didRemove;
+ RemoveEntries(aIDs, aStartIndex, &didRemove);
+ if (didRemove) {
+ RefPtr<BrowsingContext> rootBC = GetBrowsingContext();
+ if (rootBC && rootBC->GetDocShell()) {
+ rootBC->GetDocShell()->DispatchLocationChangeEvent();
+ }
+ }
+}
+
+void nsSHistory::RemoveEntries(nsTArray<nsID>& aIDs, int32_t aStartIndex,
+ bool* aDidRemove) {
+ SHistoryChangeNotifier change(this);
+
+ int32_t index = aStartIndex;
+ while (index >= 0 && RemoveChildEntries(this, --index, aIDs)) {
+ }
+ int32_t minIndex = index;
+ index = aStartIndex;
+ while (index >= 0 && RemoveChildEntries(this, index++, aIDs)) {
+ }
+
+ // We need to remove duplicate nsSHEntry trees.
+ *aDidRemove = false;
+ while (index > minIndex) {
+ if (index != mIndex && RemoveDuplicate(index, index < mIndex)) {
+ *aDidRemove = true;
+ }
+ --index;
+ }
+
+ UpdateRootBrowsingContextState();
+}
+
+void nsSHistory::RemoveFrameEntries(nsISHEntry* aEntry) {
+ int32_t count = aEntry->GetChildCount();
+ AutoTArray<nsID, 16> ids;
+ for (int32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsISHEntry> child;
+ aEntry->GetChildAt(i, getter_AddRefs(child));
+ if (child) {
+ child->GetDocshellID(*ids.AppendElement());
+ }
+ }
+ RemoveEntries(ids, mIndex);
+}
+
+void nsSHistory::RemoveDynEntries(int32_t aIndex, nsISHEntry* aEntry) {
+ // Remove dynamic entries which are at the index and belongs to the container.
+ nsCOMPtr<nsISHEntry> entry(aEntry);
+ if (!entry) {
+ GetEntryAtIndex(aIndex, getter_AddRefs(entry));
+ }
+
+ if (entry) {
+ AutoTArray<nsID, 16> toBeRemovedEntries;
+ GetDynamicChildren(entry, toBeRemovedEntries);
+ if (toBeRemovedEntries.Length()) {
+ RemoveEntries(toBeRemovedEntries, aIndex);
+ }
+ }
+}
+
+void nsSHistory::RemoveDynEntriesForBFCacheEntry(nsIBFCacheEntry* aBFEntry) {
+ int32_t index;
+ nsCOMPtr<nsISHEntry> shEntry;
+ FindEntryForBFCache(static_cast<nsSHEntryShared*>(aBFEntry),
+ getter_AddRefs(shEntry), &index);
+ if (shEntry) {
+ RemoveDynEntries(index, shEntry);
+ }
+}
+
+NS_IMETHODIMP
+nsSHistory::UpdateIndex() {
+ SHistoryChangeNotifier change(this);
+
+ // Update the actual index with the right value.
+ if (mIndex != mRequestedIndex && mRequestedIndex != -1) {
+ mIndex = mRequestedIndex;
+ }
+
+ mRequestedIndex = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::GotoIndex(int32_t aIndex, bool aUserActivation) {
+ nsTArray<LoadEntryResult> loadResults;
+ nsresult rv = GotoIndex(aIndex, loadResults, /*aSameEpoch*/ false,
+ aIndex == mIndex, aUserActivation);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LoadURIs(loadResults);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+nsSHistory::EnsureCorrectEntryAtCurrIndex(nsISHEntry* aEntry) {
+ int index = mRequestedIndex == -1 ? mIndex : mRequestedIndex;
+ if (index > -1 && (mEntries[index] != aEntry)) {
+ ReplaceEntry(index, aEntry);
+ }
+}
+
+nsresult nsSHistory::GotoIndex(int32_t aIndex,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aSameEpoch, bool aLoadCurrentEntry,
+ bool aUserActivation) {
+ return LoadEntry(aIndex, LOAD_HISTORY, HIST_CMD_GOTOINDEX, aLoadResults,
+ aSameEpoch, aLoadCurrentEntry, aUserActivation);
+}
+
+NS_IMETHODIMP_(bool)
+nsSHistory::HasUserInteractionAtIndex(int32_t aIndex) {
+ nsCOMPtr<nsISHEntry> entry;
+ GetEntryAtIndex(aIndex, getter_AddRefs(entry));
+ if (!entry) {
+ return false;
+ }
+ return entry->GetHasUserInteraction();
+}
+
+nsresult nsSHistory::LoadNextPossibleEntry(
+ int32_t aNewIndex, long aLoadType, uint32_t aHistCmd,
+ nsTArray<LoadEntryResult>& aLoadResults, bool aLoadCurrentEntry,
+ bool aUserActivation) {
+ mRequestedIndex = -1;
+ if (aNewIndex < mIndex) {
+ return LoadEntry(aNewIndex - 1, aLoadType, aHistCmd, aLoadResults,
+ /*aSameEpoch*/ false, aLoadCurrentEntry, aUserActivation);
+ }
+ if (aNewIndex > mIndex) {
+ return LoadEntry(aNewIndex + 1, aLoadType, aHistCmd, aLoadResults,
+ /*aSameEpoch*/ false, aLoadCurrentEntry, aUserActivation);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsSHistory::LoadEntry(int32_t aIndex, long aLoadType,
+ uint32_t aHistCmd,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aSameEpoch, bool aLoadCurrentEntry,
+ bool aUserActivation) {
+ MOZ_LOG(gSHistoryLog, LogLevel::Debug,
+ ("LoadEntry(%d, 0x%lx, %u)", aIndex, aLoadType, aHistCmd));
+ RefPtr<BrowsingContext> rootBC = GetBrowsingContext();
+ if (!rootBC) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aIndex < 0 || aIndex >= Length()) {
+ MOZ_LOG(gSHistoryLog, LogLevel::Debug, ("Index out of range"));
+ // The index is out of range.
+ // Clear the requested index in case it had bogus value. This way the next
+ // load succeeds if the offset is reasonable.
+ mRequestedIndex = -1;
+
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t originalRequestedIndex = mRequestedIndex;
+ int32_t previousRequest = mRequestedIndex > -1 ? mRequestedIndex : mIndex;
+ int32_t requestedOffset = aIndex - previousRequest;
+
+ // This is a normal local history navigation.
+
+ nsCOMPtr<nsISHEntry> prevEntry;
+ nsCOMPtr<nsISHEntry> nextEntry;
+ GetEntryAtIndex(mIndex, getter_AddRefs(prevEntry));
+ GetEntryAtIndex(aIndex, getter_AddRefs(nextEntry));
+ if (!nextEntry || !prevEntry) {
+ mRequestedIndex = -1;
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mozilla::SessionHistoryInParent()) {
+ if (aHistCmd == HIST_CMD_GOTOINDEX) {
+ // https://html.spec.whatwg.org/#history-traversal:
+ // To traverse the history
+ // "If entry has a different Document object than the current entry, then
+ // run the following substeps: Remove any tasks queued by the history
+ // traversal task source..."
+ //
+ // aSameEpoch is true only if the navigations would have been
+ // generated in the same spin of the event loop (i.e. history.go(-2);
+ // history.go(-1)) and from the same content process.
+ if (aSameEpoch) {
+ bool same_doc = false;
+ prevEntry->SharesDocumentWith(nextEntry, &same_doc);
+ if (!same_doc) {
+ MOZ_LOG(
+ gSHistoryLog, LogLevel::Debug,
+ ("Aborting GotoIndex %d - same epoch and not same doc", aIndex));
+ // Ignore this load. Before SessionHistoryInParent, this would
+ // have been dropped in InternalLoad after we filter out SameDoc
+ // loads.
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+ }
+ // Keep note of requested history index in mRequestedIndex; after all bailouts
+ mRequestedIndex = aIndex;
+
+ // Remember that this entry is getting loaded at this point in the sequence
+
+ nextEntry->SetLastTouched(++gTouchCounter);
+
+ // Get the uri for the entry we are about to visit
+ nsCOMPtr<nsIURI> nextURI = nextEntry->GetURI();
+
+ MOZ_ASSERT(nextURI, "nextURI can't be null");
+
+ // Send appropriate listener notifications.
+ if (aHistCmd == HIST_CMD_GOTOINDEX) {
+ // We are going somewhere else. This is not reload either
+ NotifyListeners(mListeners, [](auto l) { l->OnHistoryGotoIndex(); });
+ }
+
+ if (mRequestedIndex == mIndex) {
+ // Possibly a reload case
+ InitiateLoad(nextEntry, rootBC, aLoadType, aLoadResults, aLoadCurrentEntry,
+ aUserActivation, requestedOffset);
+ return NS_OK;
+ }
+
+ // Going back or forward.
+ bool differenceFound = LoadDifferingEntries(
+ prevEntry, nextEntry, rootBC, aLoadType, aLoadResults, aLoadCurrentEntry,
+ aUserActivation, requestedOffset);
+ if (!differenceFound) {
+ // LoadNextPossibleEntry will change the offset by one, and in order
+ // to keep track of the requestedOffset, need to reset mRequestedIndex to
+ // the value it had initially.
+ mRequestedIndex = originalRequestedIndex;
+ // We did not find any differences. Go further in the history.
+ return LoadNextPossibleEntry(aIndex, aLoadType, aHistCmd, aLoadResults,
+ aLoadCurrentEntry, aUserActivation);
+ }
+
+ return NS_OK;
+}
+
+bool nsSHistory::LoadDifferingEntries(nsISHEntry* aPrevEntry,
+ nsISHEntry* aNextEntry,
+ BrowsingContext* aParent, long aLoadType,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aLoadCurrentEntry,
+ bool aUserActivation, int32_t aOffset) {
+ MOZ_ASSERT(aPrevEntry && aNextEntry && aParent);
+
+ uint32_t prevID = aPrevEntry->GetID();
+ uint32_t nextID = aNextEntry->GetID();
+
+ // Check the IDs to verify if the pages are different.
+ if (prevID != nextID) {
+ // Set the Subframe flag if not navigating the root docshell.
+ aNextEntry->SetIsSubFrame(aParent->Id() != mRootBC);
+ InitiateLoad(aNextEntry, aParent, aLoadType, aLoadResults,
+ aLoadCurrentEntry, aUserActivation, aOffset);
+ return true;
+ }
+
+ // The entries are the same, so compare any child frames
+ int32_t pcnt = aPrevEntry->GetChildCount();
+ int32_t ncnt = aNextEntry->GetChildCount();
+
+ // Create an array for child browsing contexts.
+ nsTArray<RefPtr<BrowsingContext>> browsingContexts;
+ aParent->GetChildren(browsingContexts);
+
+ // Search for something to load next.
+ bool differenceFound = false;
+ for (int32_t i = 0; i < ncnt; ++i) {
+ // First get an entry which may cause a new page to be loaded.
+ nsCOMPtr<nsISHEntry> nChild;
+ aNextEntry->GetChildAt(i, getter_AddRefs(nChild));
+ if (!nChild) {
+ continue;
+ }
+ nsID docshellID;
+ nChild->GetDocshellID(docshellID);
+
+ // Then find the associated docshell.
+ RefPtr<BrowsingContext> bcChild;
+ for (const RefPtr<BrowsingContext>& bc : browsingContexts) {
+ if (bc->GetHistoryID() == docshellID) {
+ bcChild = bc;
+ break;
+ }
+ }
+ if (!bcChild) {
+ continue;
+ }
+
+ // Then look at the previous entries to see if there was
+ // an entry for the docshell.
+ nsCOMPtr<nsISHEntry> pChild;
+ for (int32_t k = 0; k < pcnt; ++k) {
+ nsCOMPtr<nsISHEntry> child;
+ aPrevEntry->GetChildAt(k, getter_AddRefs(child));
+ if (child) {
+ nsID dID;
+ child->GetDocshellID(dID);
+ if (dID == docshellID) {
+ pChild = child;
+ break;
+ }
+ }
+ }
+ if (!pChild) {
+ continue;
+ }
+
+ // Finally recursively call this method.
+ // This will either load a new page to shell or some subshell or
+ // do nothing.
+ if (LoadDifferingEntries(pChild, nChild, bcChild, aLoadType, aLoadResults,
+ aLoadCurrentEntry, aUserActivation, aOffset)) {
+ differenceFound = true;
+ }
+ }
+ return differenceFound;
+}
+
+void nsSHistory::InitiateLoad(nsISHEntry* aFrameEntry,
+ BrowsingContext* aFrameBC, long aLoadType,
+ nsTArray<LoadEntryResult>& aLoadResults,
+ bool aLoadCurrentEntry, bool aUserActivation,
+ int32_t aOffset) {
+ MOZ_ASSERT(aFrameBC && aFrameEntry);
+
+ LoadEntryResult* loadResult = aLoadResults.AppendElement();
+ loadResult->mBrowsingContext = aFrameBC;
+
+ nsCOMPtr<nsIURI> newURI = aFrameEntry->GetURI();
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(newURI);
+
+ loadState->SetHasValidUserGestureActivation(aUserActivation);
+
+ // At the time we initiate a history entry load we already know if https-first
+ // was able to upgrade the request from http to https. There is no point in
+ // re-retrying to upgrade.
+ loadState->SetIsExemptFromHTTPSOnlyMode(true);
+
+ /* Set the loadType in the SHEntry too to what was passed on.
+ * This will be passed on to child subframes later in nsDocShell,
+ * so that proper loadType is maintained through out a frameset
+ */
+ aFrameEntry->SetLoadType(aLoadType);
+
+ loadState->SetLoadType(aLoadType);
+
+ loadState->SetSHEntry(aFrameEntry);
+
+ // If we're loading the current entry we want to treat it as not a
+ // same-document navigation (see nsDocShell::IsSameDocumentNavigation), so
+ // record that here in the LoadingSessionHistoryEntry.
+ bool loadingCurrentEntry;
+ if (mozilla::SessionHistoryInParent()) {
+ loadingCurrentEntry = aLoadCurrentEntry;
+ } else {
+ loadingCurrentEntry =
+ aFrameBC->GetDocShell() &&
+ nsDocShell::Cast(aFrameBC->GetDocShell())->IsOSHE(aFrameEntry);
+ }
+ loadState->SetLoadIsFromSessionHistory(aOffset, loadingCurrentEntry);
+
+ if (mozilla::SessionHistoryInParent()) {
+ nsCOMPtr<SessionHistoryEntry> she = do_QueryInterface(aFrameEntry);
+ aFrameBC->Canonical()->AddLoadingSessionHistoryEntry(
+ loadState->GetLoadingSessionHistoryInfo()->mLoadId, she);
+ }
+
+ nsCOMPtr<nsIURI> originalURI = aFrameEntry->GetOriginalURI();
+ loadState->SetOriginalURI(originalURI);
+
+ loadState->SetLoadReplace(aFrameEntry->GetLoadReplace());
+
+ loadState->SetLoadFlags(nsIWebNavigation::LOAD_FLAGS_NONE);
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal =
+ aFrameEntry->GetTriggeringPrincipal();
+ loadState->SetTriggeringPrincipal(triggeringPrincipal);
+ loadState->SetFirstParty(false);
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aFrameEntry->GetCsp();
+ loadState->SetCsp(csp);
+
+ loadResult->mLoadState = std::move(loadState);
+}
+
+NS_IMETHODIMP
+nsSHistory::CreateEntry(nsISHEntry** aEntry) {
+ nsCOMPtr<nsISHEntry> entry;
+ if (XRE_IsParentProcess() && mozilla::SessionHistoryInParent()) {
+ entry = new SessionHistoryEntry();
+ } else {
+ entry = new nsSHEntry();
+ }
+ entry.forget(aEntry);
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(bool)
+nsSHistory::IsEmptyOrHasEntriesForSingleTopLevelPage() {
+ if (mEntries.IsEmpty()) {
+ return true;
+ }
+
+ nsISHEntry* entry = mEntries[0];
+ size_t length = mEntries.Length();
+ for (size_t i = 1; i < length; ++i) {
+ bool sharesDocument = false;
+ mEntries[i]->SharesDocumentWith(entry, &sharesDocument);
+ if (!sharesDocument) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void CollectEntries(
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable,
+ SessionHistoryEntry* aEntry) {
+ aHashtable.InsertOrUpdate(aEntry->DocshellID(), aEntry);
+ for (const RefPtr<SessionHistoryEntry>& entry : aEntry->Children()) {
+ if (entry) {
+ CollectEntries(aHashtable, entry);
+ }
+ }
+}
+
+static void UpdateEntryLength(
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*>& aHashtable,
+ SessionHistoryEntry* aNewEntry, bool aMove) {
+ SessionHistoryEntry* oldEntry = aHashtable.Get(aNewEntry->DocshellID());
+ if (oldEntry) {
+ MOZ_ASSERT(oldEntry->GetID() != aNewEntry->GetID() || !aMove ||
+ !aNewEntry->BCHistoryLength().Modified());
+ aNewEntry->SetBCHistoryLength(oldEntry->BCHistoryLength());
+ if (oldEntry->GetID() != aNewEntry->GetID()) {
+ MOZ_ASSERT(!aMove);
+ // If we have a new id then aNewEntry was created for a new load, so
+ // record that in BCHistoryLength.
+ ++aNewEntry->BCHistoryLength();
+ } else if (aMove) {
+ // We're moving the BCHistoryLength from the old entry to the new entry,
+ // so we need to let the old entry know that it's not contributing to its
+ // BCHistoryLength, and the new one that it does if the old one was
+ // before.
+ aNewEntry->BCHistoryLength().SetModified(
+ oldEntry->BCHistoryLength().Modified());
+ oldEntry->BCHistoryLength().SetModified(false);
+ }
+ }
+
+ for (const RefPtr<SessionHistoryEntry>& entry : aNewEntry->Children()) {
+ if (entry) {
+ UpdateEntryLength(aHashtable, entry, aMove);
+ }
+ }
+}
+
+void nsSHistory::UpdateEntryLength(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry,
+ bool aMove) {
+ nsCOMPtr<SessionHistoryEntry> oldSHE = do_QueryInterface(aOldEntry);
+ nsCOMPtr<SessionHistoryEntry> newSHE = do_QueryInterface(aNewEntry);
+
+ if (!oldSHE || !newSHE) {
+ return;
+ }
+
+ nsTHashMap<nsIDHashKey, SessionHistoryEntry*> docshellIDToEntry;
+ CollectEntries(docshellIDToEntry, oldSHE);
+
+ ::UpdateEntryLength(docshellIDToEntry, newSHE, aMove);
+}