/* -*- 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 #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 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 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 uri = shentry->GetURI(); \ LOG_SPEC(format, uri); \ } \ PR_END_MACRO // Calls a F on all registered session history listeners. template static void NotifyListeners(nsAutoTObserverArray& aListeners, F&& f) { for (const nsWeakPtr& weakPtr : aListeners.EndLimitedRange()) { nsCOMPtr 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 rootBC = mSHistory->GetBrowsingContext(); if (mozilla::SessionHistoryInParent() && rootBC) { rootBC->Canonical()->HistoryCommitIndexAndLength(); } } } RefPtr 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 gObserver; NS_IMPL_ISUPPORTS(nsSHistoryObserver, nsIObserver) // static void nsSHistoryObserver::PrefChanged(const char* aPref, void* aSelf) { static_cast(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 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 she = do_QueryInterface(aEntry)) { if (RefPtr frameLoader = she->GetFrameLoader()) { nsCOMPtr owner = do_QueryInterface(frameLoader->GetOwnerContent()); RefPtr 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( 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 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 obsSvc = mozilla::services::GetObserverService(); if (obsSvc) { obsSvc->RemoveObserver(gObserver, "cacheservice:empty-cache"); obsSvc->RemoveObserver(gObserver, "memory-pressure"); } gObserver = nullptr; } } // static already_AddRefed nsSHistory::GetRootSHEntry(nsISHEntry* aEntry) { nsCOMPtr rootEntry = aEntry; nsCOMPtr 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 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(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 resultEntry; }; nsresult nsSHistory::CloneAndReplaceChild(nsISHEntry* aEntry, BrowsingContext* aOwnerBC, int32_t aChildIndex, void* aData) { nsCOMPtr dest; CloneAndReplaceData* data = static_cast(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& aCallback) { MOZ_ASSERT(aEntry); nsCOMPtr 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 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 entry; shistory->GetEntryAtIndex(i, getter_AddRefs(entry)); if (entry) { nsCOMPtr 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 entry; shistory->GetEntryAtIndex(i, getter_AddRefs(entry)); if (entry) { nsCOMPtr 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 child; nsCOMPtr 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(aData); if (!aBC || aBC == data->ignoreBC) { return NS_OK; } nsISHEntry* destTreeRoot = data->destTreeRoot; nsCOMPtr 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 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(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* aPreviousEntryIndex, Maybe* 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 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 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 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 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 uri = aEntry->GetURI(); nsAutoString title, name; aEntry->GetTitle(title); aEntry->GetName(name); SHEntrySharedParentState* shared; if (mozilla::SessionHistoryInParent()) { shared = static_cast(aEntry)->SharedInfo(); } else { shared = static_cast(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 prevChild; for (int32_t i = 0; i < childCount; ++i) { nsCOMPtr 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& aHashtable) { if (!aEntry->BCHistoryLength().Modified()) { ++(aEntry->BCHistoryLength()); } aHashtable.InsertOrUpdate(aEntry->DocshellID(), aEntry); for (const RefPtr& entry : aEntry->Children()) { if (entry) { MarkAsInitialEntry(entry, aHashtable); } } } static void ClearEntries(SessionHistoryEntry* aEntry) { aEntry->ClearBCHistoryLength(); for (const RefPtr& 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 docshellIDToEntry; if (aNumEntries != Length()) { nsCOMPtr 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 she = do_QueryInterface(mEntries[i]); if (she) { ClearEntries(she); } } RefPtr 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 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 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 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 shistory = aEntry->GetShistory(); int32_t indexOfHistoryLoad = shistory ? shistory->GetIndexOfEntry(aEntry) : -1; nsCOMPtr frameLoaderOwner = do_QueryInterface(aBrowsingContext->GetEmbedderElement()); if (frameLoaderOwner && aFrameLoader->GetMaybePendingBrowsingContext() && indexOfHistoryLoad >= 0) { RefPtr 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 nextURI = aEntry->GetURI(); nsCOMPtr nextOriginalURI = aEntry->GetOriginalURI(); nsCOMPtr request = MakeAndAddRef( 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 loadingBC = aFrameLoader->GetMaybePendingBrowsingContext()->Canonical(); RefPtr currentFrameLoader = frameLoaderOwner->GetFrameLoader(); // The current page can be bfcached, store the // nsFrameLoader in the current SessionHistoryEntry. RefPtr 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 loadState = aLoadEntry.mLoadState; RefPtr canonicalBC = aLoadEntry.mBrowsingContext->Canonical(); nsCOMPtr she = do_QueryInterface(loadState->SHEntry()); nsCOMPtr currentShe = canonicalBC->GetActiveSessionHistoryEntry(); MOZ_ASSERT(she); RefPtr 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 frameLoaderOwner = do_QueryInterface(canonicalBC->GetEmbedderElement()); if (frameLoaderOwner) { RefPtr 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 bc = aLoadEntry.mBrowsingContext; RefPtr loadState = aLoadEntry.mLoadState; bc->LoadURI(loadState, false); } /* static */ void nsSHistory::LoadURIs(nsTArray& aLoadResults) { for (LoadEntryResult& loadEntry : aLoadResults) { LoadURIOrBFCache(loadEntry); } } NS_IMETHODIMP nsSHistory::Reload(uint32_t aReloadFlags) { nsTArray 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& 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 loadResults; nsresult rv = ReloadCurrentEntry(loadResults); NS_ENSURE_SUCCESS(rv, rv); LoadURIs(loadResults); return NS_OK; } nsresult nsSHistory::ReloadCurrentEntry( nsTArray& 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 safeViewers; nsTArray> safeFrameLoaders; for (int32_t i = startSafeIndex; i <= endSafeIndex; i++) { nsCOMPtr viewer = mEntries[i]->GetContentViewer(); if (viewer) { safeViewers.AppendObject(viewer); } else if (nsCOMPtr 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 entry = mEntries[i]; nsCOMPtr viewer = entry->GetContentViewer(); if (viewer) { if (safeViewers.IndexOf(viewer) == -1) { EvictContentViewerForEntry(entry); } } else if (nsCOMPtr 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 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 mSHistory; nsCOMPtr mEntry; nsCOMPtr mViewer; RefPtr 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 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 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 entry = shist->mEntries[i]; nsCOMPtr 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 she = do_QueryInterface(entry)) { if (RefPtr 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 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 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 entry = aEntry; if (!mHistoryTracker || !entry) { return; } mHistoryTracker->AddObject(entry); return; } NS_IMETHODIMP_(void) nsSHistory::RemoveFromExpirationTracker(SHEntrySharedParentState* aEntry) { RefPtr 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& aDocshellIDs) { int32_t count = aEntry->GetChildCount(); for (int32_t i = 0; i < count; ++i) { nsCOMPtr 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& aDocshellIDs) { bool didRemove = false; int32_t childCount = aRoot->GetChildCount(); for (int32_t i = childCount - 1; i >= 0; --i) { nsCOMPtr 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& aEntryIDs) { nsCOMPtr 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 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 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 she = do_QueryInterface(root1); if (she) { ClearEntries(she); } mEntries.RemoveElementAt(aIndex); // FIXME Bug 1546350: Reimplement history listeners. // if (mRootBC && mRootBC->GetDocShell()) { // static_cast(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& aIDs, int32_t aStartIndex) { bool didRemove; RemoveEntries(aIDs, aStartIndex, &didRemove); if (didRemove) { RefPtr rootBC = GetBrowsingContext(); if (rootBC && rootBC->GetDocShell()) { rootBC->GetDocShell()->DispatchLocationChangeEvent(); } } } void nsSHistory::RemoveEntries(nsTArray& 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 ids; for (int32_t i = 0; i < count; ++i) { nsCOMPtr 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 entry(aEntry); if (!entry) { GetEntryAtIndex(aIndex, getter_AddRefs(entry)); } if (entry) { AutoTArray toBeRemovedEntries; GetDynamicChildren(entry, toBeRemovedEntries); if (toBeRemovedEntries.Length()) { RemoveEntries(toBeRemovedEntries, aIndex); } } } void nsSHistory::RemoveDynEntriesForBFCacheEntry(nsIBFCacheEntry* aBFEntry) { int32_t index; nsCOMPtr shEntry; FindEntryForBFCache(static_cast(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 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& 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 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& 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& aLoadResults, bool aSameEpoch, bool aLoadCurrentEntry, bool aUserActivation) { MOZ_LOG(gSHistoryLog, LogLevel::Debug, ("LoadEntry(%d, 0x%lx, %u)", aIndex, aLoadType, aHistCmd)); RefPtr 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 prevEntry; nsCOMPtr 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 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& 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> 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 nChild; aNextEntry->GetChildAt(i, getter_AddRefs(nChild)); if (!nChild) { continue; } nsID docshellID; nChild->GetDocshellID(docshellID); // Then find the associated docshell. RefPtr bcChild; for (const RefPtr& 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 pChild; for (int32_t k = 0; k < pcnt; ++k) { nsCOMPtr 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& aLoadResults, bool aLoadCurrentEntry, bool aUserActivation, int32_t aOffset) { MOZ_ASSERT(aFrameBC && aFrameEntry); LoadEntryResult* loadResult = aLoadResults.AppendElement(); loadResult->mBrowsingContext = aFrameBC; nsCOMPtr newURI = aFrameEntry->GetURI(); RefPtr 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 she = do_QueryInterface(aFrameEntry); aFrameBC->Canonical()->AddLoadingSessionHistoryEntry( loadState->GetLoadingSessionHistoryInfo()->mLoadId, she); } nsCOMPtr originalURI = aFrameEntry->GetOriginalURI(); loadState->SetOriginalURI(originalURI); loadState->SetLoadReplace(aFrameEntry->GetLoadReplace()); loadState->SetLoadFlags(nsIWebNavigation::LOAD_FLAGS_NONE); nsCOMPtr triggeringPrincipal = aFrameEntry->GetTriggeringPrincipal(); loadState->SetTriggeringPrincipal(triggeringPrincipal); loadState->SetFirstParty(false); nsCOMPtr csp = aFrameEntry->GetCsp(); loadState->SetCsp(csp); loadResult->mLoadState = std::move(loadState); } NS_IMETHODIMP nsSHistory::CreateEntry(nsISHEntry** aEntry) { nsCOMPtr 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& aHashtable, SessionHistoryEntry* aEntry) { aHashtable.InsertOrUpdate(aEntry->DocshellID(), aEntry); for (const RefPtr& entry : aEntry->Children()) { if (entry) { CollectEntries(aHashtable, entry); } } } static void UpdateEntryLength( nsTHashMap& 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& entry : aNewEntry->Children()) { if (entry) { UpdateEntryLength(aHashtable, entry, aMove); } } } void nsSHistory::UpdateEntryLength(nsISHEntry* aOldEntry, nsISHEntry* aNewEntry, bool aMove) { nsCOMPtr oldSHE = do_QueryInterface(aOldEntry); nsCOMPtr newSHE = do_QueryInterface(aNewEntry); if (!oldSHE || !newSHE) { return; } nsTHashMap docshellIDToEntry; CollectEntries(docshellIDToEntry, oldSHE); ::UpdateEntryLength(docshellIDToEntry, newSHE, aMove); }