diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/sessionstore | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/sessionstore')
28 files changed, 5707 insertions, 0 deletions
diff --git a/toolkit/components/sessionstore/BrowserSessionStore.cpp b/toolkit/components/sessionstore/BrowserSessionStore.cpp new file mode 100644 index 0000000000..f4da9646b2 --- /dev/null +++ b/toolkit/components/sessionstore/BrowserSessionStore.cpp @@ -0,0 +1,292 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/BrowserSessionStore.h" +#include <algorithm> +#include <cstdint> +#include <functional> + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPtr.h" + +#include "mozilla/dom/BrowserSessionStoreBinding.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/SessionStoreFormData.h" +#include "mozilla/dom/SessionStoreScrollData.h" +#include "mozilla/dom/WindowGlobalParent.h" + +#include "nsTHashMap.h" +#include "nsHashtablesFwd.h" + +#include "js/RootingAPI.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static StaticAutoPtr<nsTHashMap<nsUint64HashKey, BrowserSessionStore*>> + sSessionStore; + +NS_IMPL_CYCLE_COLLECTION(BrowserSessionStore, mBrowsingContext, mFormData, + mScrollData) + +/* static */ +already_AddRefed<BrowserSessionStore> BrowserSessionStore::GetOrCreate( + CanonicalBrowsingContext* aBrowsingContext) { + if (!aBrowsingContext->IsTop()) { + return nullptr; + } + + if (!sSessionStore) { + sSessionStore = new nsTHashMap<nsUint64HashKey, BrowserSessionStore*>(); + ClearOnShutdown(&sSessionStore); + } + + return do_AddRef(sSessionStore->LookupOrInsertWith( + aBrowsingContext->Id(), + [&] { return new BrowserSessionStore(aBrowsingContext); })); +} + +BrowserSessionStore::BrowserSessionStore( + CanonicalBrowsingContext* aBrowsingContext) + : mBrowsingContext(aBrowsingContext) {} + +SessionStoreFormData* BrowserSessionStore::GetFormdata() { return mFormData; } + +SessionStoreScrollData* BrowserSessionStore::GetScroll() { return mScrollData; } + +static bool ShouldUpdateSessionStore(CanonicalBrowsingContext* aBrowsingContext, + uint32_t aEpoch) { + if (!aBrowsingContext) { + return false; + } + + if (aBrowsingContext->Top()->GetSessionStoreEpoch() != aEpoch) { + return false; + } + + if (aBrowsingContext->IsReplaced()) { + return false; + } + + if (aBrowsingContext->IsDynamic()) { + return false; + } + + return true; +} + +// With GetOrCreate we can create either of the weak fields: +// WeakPtr<SessionStoreFormData> mFormdata; +// WeakPtr<SessionStoreScrollData> mScroll; +// in CanonicalBrowsingContext. If one already exists, then we return that. +template <typename T, WeakPtr<T>& (CanonicalBrowsingContext::*GetWeakRef)()> +static already_AddRefed<T> GetOrCreateEntry( + CanonicalBrowsingContext* aBrowsingContext) { + typename T::LocationType& location = (aBrowsingContext->*GetWeakRef)(); + RefPtr<T> entry = location.get(); + if (!entry) { + entry = MakeRefPtr<T>(); + location = entry; + } + + return entry.forget(); +} + +// With InsertEntry we can insert an entry in the session store data tree in +// either of the weak fields: +// WeakPtr<SessionStoreFormData> mFormdata; +// WeakPtr<SessionStoreScrollData> mScroll; +// in CanonicalBrowsingContext. If an entry is inserted where there is no parent +// entry, a spine of entries will be created until one is found, or we reach the +// top browsing context. +template <typename T> +void InsertEntry(BrowsingContext* aBrowsingContext, T* aParent, T* aUpdate) { + int32_t offset = aBrowsingContext->ChildOffset(); + if (offset < 0) { + return; + } + + aParent->ClearCachedChildren(); + + auto& children = aParent->Children(); + + children.EnsureLengthAtLeast(offset + 1); + if (children[offset] && !aBrowsingContext->Children().IsEmpty()) { + children[offset]->ClearCachedChildren(); + aUpdate->ClearCachedChildren(); + } + + children[offset] = aUpdate; +} + +// With RemoveEntry we can remove an entry in the session store data tree in +// either of the weak fields: +// WeakPtr<SessionStoreFormData> mFormdata; +// WeakPtr<SessionStoreScrollData> mScroll; +// in CanonicalBrowsingContext. If an entry is removed, where its parent doesn't +// contain data, we'll remove the parent and repeat until we either find an +// entry with data or reach the top browsing context. +template <typename T> +void RemoveEntry(BrowsingContext* aBrowsingContext, T* aParent) { + int32_t offset = aBrowsingContext->ChildOffset(); + if (offset < 0) { + return; + } + + if (!aParent) { + return; + } + + aParent->ClearCachedChildren(); + + auto& children = aParent->Children(); + size_t length = children.Length(); + if (children.Length() <= static_cast<size_t>(offset)) { + // The children array doesn't extend to offset. + return; + } + + if (static_cast<size_t>(offset) < length - 1) { + // offset is before the last item in the children array. + children[offset] = nullptr; + return; + } + + // offset is the last item, find the first non-null item before it + // and remove anything after that item. + while (offset > 0 && !children[offset - 1]) { + --offset; + } + + children.TruncateLength(offset); +} + +// With UpdateSessionStoreField we can update an entry in the session store +// data tree in either of the weak fields: +// WeakPtr<SessionStoreFormData> mFormdata; +// WeakPtr<SessionStoreScrollData> mScroll; +// in CanonicalBrowsingContext. UpdateSessionStoreField uses the above +// functions, `GetOrCreateEntry`, `InsertEntry` and `RemoveEntry` to operate on +// the weak fields. We return the top-level entry attached to the top browsing +// context through `aEntry`. If the entire browsing context tree contains no +// session store data this will be set to nullptr. +template <typename T, WeakPtr<T>& (CanonicalBrowsingContext::*GetWeakRef)()> +void UpdateSessionStoreField(CanonicalBrowsingContext* aBrowsingContext, + const typename T::CollectedType& aUpdate, + T** aEntry) { + RefPtr<T> currentEntry; + + if (T::HasData(aUpdate)) { + currentEntry = GetOrCreateEntry<T, GetWeakRef>(aBrowsingContext); + currentEntry->Update(aUpdate); + + CanonicalBrowsingContext* currentBrowsingContext = aBrowsingContext; + while (CanonicalBrowsingContext* parent = + currentBrowsingContext->GetParent()) { + WeakPtr<T>& parentEntry = (parent->*GetWeakRef)(); + if (parentEntry) { + InsertEntry(aBrowsingContext, parentEntry.get(), currentEntry.get()); + break; + } + + RefPtr<T> entry = GetOrCreateEntry<T, GetWeakRef>(parent); + InsertEntry(currentBrowsingContext, entry.get(), currentEntry.get()); + + currentEntry = entry; + currentBrowsingContext = parent; + } + + currentEntry = (aBrowsingContext->Top()->*GetWeakRef)().get(); + } else if ((currentEntry = (aBrowsingContext->*GetWeakRef)())) { + currentEntry->Update(aUpdate); + + CanonicalBrowsingContext* currentBrowsingContext = aBrowsingContext; + while (CanonicalBrowsingContext* parent = + currentBrowsingContext->GetParent()) { + if (!currentEntry || !currentEntry->IsEmpty()) { + break; + } + + T* parentEntry = (parent->*GetWeakRef)().get(); + RemoveEntry(currentBrowsingContext, parentEntry); + + currentEntry = parentEntry; + currentBrowsingContext = parent; + } + + if (currentEntry && currentEntry->IsEmpty()) { + currentEntry = nullptr; + } else { + currentEntry = (aBrowsingContext->Top()->*GetWeakRef)().get(); + } + } else { + currentEntry = (aBrowsingContext->Top()->*GetWeakRef)().get(); + } + + *aEntry = currentEntry.forget().take(); +} + +void BrowserSessionStore::UpdateSessionStore( + CanonicalBrowsingContext* aBrowsingContext, + const Maybe<sessionstore::FormData>& aFormData, + const Maybe<nsPoint>& aScrollPosition, uint32_t aEpoch) { + if (!aFormData && !aScrollPosition) { + return; + } + + if (!ShouldUpdateSessionStore(aBrowsingContext, aEpoch)) { + return; + } + + if (aFormData) { + UpdateSessionStoreField< + SessionStoreFormData, + &CanonicalBrowsingContext::GetSessionStoreFormDataRef>( + aBrowsingContext, *aFormData, getter_AddRefs(mFormData)); + } + + if (aScrollPosition) { + UpdateSessionStoreField< + SessionStoreScrollData, + &CanonicalBrowsingContext::GetSessionStoreScrollDataRef>( + aBrowsingContext, *aScrollPosition, getter_AddRefs(mScrollData)); + } +} + +void BrowserSessionStore::RemoveSessionStore( + CanonicalBrowsingContext* aBrowsingContext) { + if (!aBrowsingContext) { + return; + } + + CanonicalBrowsingContext* parentContext = aBrowsingContext->GetParent(); + + if (parentContext) { + RemoveEntry(aBrowsingContext, + parentContext->GetSessionStoreFormDataRef().get()); + + RemoveEntry(aBrowsingContext, + parentContext->GetSessionStoreScrollDataRef().get()); + + return; + } + + if (aBrowsingContext->IsTop()) { + mFormData = nullptr; + mScrollData = nullptr; + } +} + +BrowserSessionStore::~BrowserSessionStore() { + if (sSessionStore) { + sSessionStore->Remove(mBrowsingContext->Id()); + } +} diff --git a/toolkit/components/sessionstore/BrowserSessionStore.h b/toolkit/components/sessionstore/BrowserSessionStore.h new file mode 100644 index 0000000000..011e7673ec --- /dev/null +++ b/toolkit/components/sessionstore/BrowserSessionStore.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStore_h +#define mozilla_dom_SessionStore_h + +#include "nsWrapperCache.h" + +#include "mozilla/Maybe.h" + +struct nsPoint; + +namespace mozilla::dom { + +class CanonicalBrowsingContext; +class GlobalObject; +class SessionStoreFormData; +class SessionStoreScrollData; +class WindowGlobalParent; + +namespace sessionstore { +class FormData; +} + +class BrowserSessionStore final { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BrowserSessionStore) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(BrowserSessionStore) + + static already_AddRefed<BrowserSessionStore> GetOrCreate( + CanonicalBrowsingContext* aBrowsingContext); + + SessionStoreFormData* GetFormdata(); + SessionStoreScrollData* GetScroll(); + + void UpdateSessionStore(CanonicalBrowsingContext* aBrowsingContext, + const Maybe<sessionstore::FormData>& aFormData, + const Maybe<nsPoint>& aScrollPosition, + uint32_t aEpoch); + + void RemoveSessionStore(CanonicalBrowsingContext* aBrowsingContext); + + private: + explicit BrowserSessionStore(CanonicalBrowsingContext* aBrowsingContext); + virtual ~BrowserSessionStore(); + + RefPtr<CanonicalBrowsingContext> mBrowsingContext; + RefPtr<SessionStoreFormData> mFormData; + RefPtr<SessionStoreScrollData> mScrollData; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SessionStore_h diff --git a/toolkit/components/sessionstore/PSessionStore.ipdl b/toolkit/components/sessionstore/PSessionStore.ipdl new file mode 100644 index 0000000000..4b7bb48849 --- /dev/null +++ b/toolkit/components/sessionstore/PSessionStore.ipdl @@ -0,0 +1,63 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include "mozilla/GfxMessageUtils.h"; + +include protocol PBrowser; +include protocol PInProcess; +include SessionStoreTypes; + +using mozilla::dom::MaybeDiscardedBrowsingContext from "mozilla/dom/BrowsingContext.h"; +using mozilla::dom::MaybeSessionStoreZoom from "mozilla/dom/SessionStoreScrollData.h"; + +namespace mozilla { +namespace dom { + +/** + * The PSessionStore actor handles collection of session store data from content + * type documents. It can be used both in a content process and in the parent + * process. In particular it solves the problem of handling incremental updates + * to the session store, since we're collecting from potentially several content + * processes. + */ +async protocol PSessionStore +{ + manager PBrowser or PInProcess; + +parent: + /** + * Sends data to be stored and instructions to the session store to + * potentially collect data in the parent. This is data that is not + * collected incrementally. + */ + async SessionStoreUpdate( + nsCString? aDocShellCaps, bool? aPrivateMode, MaybeSessionStoreZoom aZoom, + bool aNeedCollectSHistory, uint32_t aEpoch); + + /** + * Sends data to be stored to the session store. The collected data + * is all the collected changed data from all the in-process documents + * in the process in which the SessionStoreChild actor lives. + */ + async IncrementalSessionStoreUpdate( + MaybeDiscardedBrowsingContext aBrowsingContext, FormData? aFormData, + nsPoint? aScrollPosition, uint32_t aEpoch); + + /** + * Drop all the collected data associated with the provided browsing + * context. + */ + async ResetSessionStore( + MaybeDiscardedBrowsingContext aBrowsingContext, uint32_t aEpoch); + +child: + async FlushTabState() returns(bool aHadContext); + + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/toolkit/components/sessionstore/RestoreTabContentObserver.cpp b/toolkit/components/sessionstore/RestoreTabContentObserver.cpp new file mode 100644 index 0000000000..b0dda66ff0 --- /dev/null +++ b/toolkit/components/sessionstore/RestoreTabContentObserver.cpp @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/Services.h" +#include "nsIObserverService.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsReadableUtils.h" +#include "nsThreadUtils.h" +#include "RestoreTabContentObserver.h" + +using namespace mozilla::dom; + +NS_IMPL_ISUPPORTS(RestoreTabContentObserver, nsIObserver) + +const char* const kAboutReaderTopic = "AboutReader:Ready"; +const char* const kContentDocumentLoaded = "content-document-loaded"; +const char* const kChromeDocumentLoaded = "chrome-document-loaded"; + +/* static */ +void RestoreTabContentObserver::Initialize() { + MOZ_ASSERT(!gRestoreTabContentObserver); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<RestoreTabContentObserver> observer = new RestoreTabContentObserver(); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return; + } + + obs->AddObserver(observer, kAboutReaderTopic, false); + obs->AddObserver(observer, kContentDocumentLoaded, false); + obs->AddObserver(observer, kChromeDocumentLoaded, false); + + gRestoreTabContentObserver = observer; +} + +/* static */ +void RestoreTabContentObserver::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!gRestoreTabContentObserver) { + return; + } + + RefPtr<RestoreTabContentObserver> observer = gRestoreTabContentObserver; + gRestoreTabContentObserver = nullptr; + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + return; + } + + obs->RemoveObserver(observer, kAboutReaderTopic); + obs->RemoveObserver(observer, kContentDocumentLoaded); + obs->RemoveObserver(observer, kChromeDocumentLoaded); +} + +NS_IMETHODIMP +RestoreTabContentObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + nsCOMPtr<nsPIDOMWindowInner> inner; + if (!strcmp(aTopic, kAboutReaderTopic)) { + inner = do_QueryInterface(aSubject); + } else if (!strcmp(aTopic, kContentDocumentLoaded) || + !strcmp(aTopic, kChromeDocumentLoaded)) { + nsCOMPtr<Document> doc = do_QueryInterface(aSubject); + inner = doc ? doc->GetInnerWindow() : nullptr; + } + if (!inner) { + return NS_OK; + } + + nsCOMPtr<nsIURI> uri = inner->GetDocumentURI(); + if (!uri) { + return NS_OK; + } + + // We'll handle loading about:reader with "AboutReader:Ready" + // rather than "content-document-loaded". + if (uri->SchemeIs("about") && + StringBeginsWith(uri->GetSpecOrDefault(), "about:reader"_ns) && + strcmp(aTopic, kAboutReaderTopic) != 0) { + return NS_OK; + } + + RefPtr<BrowsingContext> bc = inner->GetBrowsingContext(); + if (!bc || !bc->Top()->GetHasRestoreData()) { + return NS_OK; + } + if (XRE_IsParentProcess()) { + if (WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal()) { + bc->Canonical()->Top()->RequestRestoreTabContent(wgp); + } + } else if (WindowContext* windowContext = bc->GetCurrentWindowContext()) { + if (WindowGlobalChild* wgc = windowContext->GetWindowGlobalChild()) { + wgc->SendRequestRestoreTabContent(); + } + } + return NS_OK; +} + +mozilla::StaticRefPtr<RestoreTabContentObserver> + RestoreTabContentObserver::gRestoreTabContentObserver; diff --git a/toolkit/components/sessionstore/RestoreTabContentObserver.h b/toolkit/components/sessionstore/RestoreTabContentObserver.h new file mode 100644 index 0000000000..edab379a55 --- /dev/null +++ b/toolkit/components/sessionstore/RestoreTabContentObserver.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RestoreTabContentObserver_h +#define mozilla_dom_RestoreTabContentObserver_h + +#include "mozilla/StaticPtr.h" +#include "nsIObserver.h" +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace dom { + +// An observer for restoring session store data at a correct time. +class RestoreTabContentObserver final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + static void Initialize(); + static void Shutdown(); + + private: + RestoreTabContentObserver() = default; + + virtual ~RestoreTabContentObserver() = default; + static mozilla::StaticRefPtr<RestoreTabContentObserver> + gRestoreTabContentObserver; +}; +} // namespace dom +} // namespace mozilla +#endif // mozilla_dom_RestoreTabContentObserver_h diff --git a/toolkit/components/sessionstore/SessionStoreChangeListener.cpp b/toolkit/components/sessionstore/SessionStoreChangeListener.cpp new file mode 100644 index 0000000000..f9de5ec9a0 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreChangeListener.cpp @@ -0,0 +1,393 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/SessionStoreChangeListener.h" + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/SessionStoreChild.h" +#include "mozilla/dom/SessionStoreUtils.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_browser.h" + +#include "nsBaseHashtable.h" +#include "nsDocShell.h" +#include "nsGenericHTMLElement.h" +#include "nsIXULRuntime.h" +#include "nsPIDOMWindow.h" +#include "nsTHashMap.h" +#include "nsTHashtable.h" +#include "nsLayoutUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +namespace { +constexpr auto kInput = u"input"_ns; +constexpr auto kScroll = u"mozvisualscroll"_ns; +constexpr auto kResize = u"mozvisualresize"_ns; + +static constexpr char kNoAutoUpdates[] = + "browser.sessionstore.debug.no_auto_updates"; +static constexpr char kInterval[] = "browser.sessionstore.interval"; +static const char* kObservedPrefs[] = {kNoAutoUpdates, kInterval, nullptr}; +} // namespace + +inline void ImplCycleCollectionUnlink( + SessionStoreChangeListener::SessionStoreChangeTable& aField) { + aField.Clear(); +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + const SessionStoreChangeListener::SessionStoreChangeTable& aField, + const char* aName, uint32_t aFlags = 0) { + for (auto iter = aField.ConstIter(); !iter.Done(); iter.Next()) { + CycleCollectionNoteChild(aCallback, iter.Key(), aName, aFlags); + } +} + +NS_IMPL_CYCLE_COLLECTION(SessionStoreChangeListener, mBrowsingContext, + mCurrentEventTarget, mSessionStoreChild, + mSessionStoreChanges) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SessionStoreChangeListener) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(SessionStoreChangeListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SessionStoreChangeListener) + +NS_IMETHODIMP +SessionStoreChangeListener::GetName(nsACString& aName) { + aName.AssignLiteral("SessionStoreChangeListener"); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreChangeListener::Notify(nsITimer* aTimer) { + FlushSessionStore(); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreChangeListener::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + FlushSessionStore(); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreChangeListener::HandleEvent(dom::Event* aEvent) { + EventTarget* target = aEvent->GetTarget(); + if (!target) { + return NS_OK; + } + + nsIGlobalObject* global = target->GetOwnerGlobal(); + if (!global) { + return NS_OK; + } + + nsPIDOMWindowInner* inner = global->GetAsInnerWindow(); + if (!inner) { + return NS_OK; + } + + WindowContext* windowContext = inner->GetWindowContext(); + if (!windowContext) { + return NS_OK; + } + + BrowsingContext* browsingContext = windowContext->GetBrowsingContext(); + if (!browsingContext) { + return NS_OK; + } + + if (browsingContext->IsDynamic()) { + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + + if (eventType == kInput) { + RecordChange(windowContext, Change::Input); + } else if (eventType == kScroll) { + RecordChange(windowContext, Change::Scroll); + } else if (eventType == kResize && browsingContext->IsTop()) { + RecordChange(windowContext, Change::Resize); + } + return NS_OK; +} + +/* static */ already_AddRefed<SessionStoreChangeListener> +SessionStoreChangeListener::Create(BrowsingContext* aBrowsingContext) { + MOZ_RELEASE_ASSERT( + StaticPrefs::browser_sessionstore_platform_collection_AtStartup()); + if (!aBrowsingContext) { + return nullptr; + } + + RefPtr<SessionStoreChangeListener> listener = + new SessionStoreChangeListener(aBrowsingContext); + listener->Init(); + + return listener.forget(); +} + +void SessionStoreChangeListener::Stop() { + RemoveEventListeners(); + Preferences::RemoveObservers(this, kObservedPrefs); +} + +void SessionStoreChangeListener::UpdateEventTargets() { + RemoveEventListeners(); + AddEventListeners(); +} + +static void CollectFormData(Document* aDocument, + Maybe<sessionstore::FormData>& aFormData) { + aFormData.emplace(); + auto& formData = aFormData.ref(); + uint32_t size = SessionStoreUtils::CollectFormData(aDocument, formData); + + Element* body = aDocument->GetBody(); + if (aDocument->HasFlag(NODE_IS_EDITABLE) && body) { + IgnoredErrorResult result; + body->GetInnerHTML(formData.innerHTML(), result); + size += formData.innerHTML().Length(); + if (!result.Failed()) { + formData.hasData() = true; + } + } + + if (!formData.hasData()) { + return; + } + + nsIURI* documentURI = aDocument->GetDocumentURI(); + if (!documentURI) { + return; + } + + documentURI->GetSpecIgnoringRef(formData.uri()); + + if (size > StaticPrefs::browser_sessionstore_dom_form_max_limit()) { + aFormData = Nothing(); + } +} + +static void GetZoom(BrowsingContext* aBrowsingContext, + Maybe<SessionStoreZoom>& aZoom) { + nsIDocShell* docShell = aBrowsingContext->GetDocShell(); + if (!docShell) { + return; + } + + PresShell* presShell = docShell->GetPresShell(); + if (!presShell) { + return; + } + + LayoutDeviceIntSize displaySize; + + if (!nsLayoutUtils::GetDocumentViewerSize(presShell->GetPresContext(), + displaySize)) { + return; + } + + aZoom.emplace(presShell->GetResolution(), displaySize.width, + displaySize.height); +} + +void SessionStoreChangeListener::FlushSessionStore() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + bool collectSessionHistory = false; + bool collectWireFrame = false; + bool didResize = false; + + for (auto& iter : mSessionStoreChanges) { + WindowContext* windowContext = iter.GetKey(); + if (!windowContext) { + continue; + } + + BrowsingContext* browsingContext = windowContext->GetBrowsingContext(); + + // This is a bit unfortunate, but is needed when a window context with a + // recorded change has become non-current before its data has been + // collected. This can happen either due to navigation or destruction, and + // in the previous case we don't want to collect, but in the latter we do. + // This could be cleaned up if we change. See bug 1770773. + if (!windowContext->IsCurrent() && !browsingContext->IsDiscarded()) { + continue; + } + + RefPtr<Document> document = windowContext->GetDocument(); + if (!document) { + continue; + } + + EnumSet<Change> changes = iter.GetData(); + Maybe<sessionstore::FormData> maybeFormData; + if (changes.contains(Change::Input)) { + CollectFormData(document, maybeFormData); + } + + Maybe<nsPoint> maybeScroll; + PresShell* presShell = document->GetPresShell(); + + if (presShell && changes.contains(Change::Scroll)) { + maybeScroll = Some(presShell->GetVisualViewportOffset()); + } + + collectWireFrame = collectWireFrame || changes.contains(Change::WireFrame); + + collectSessionHistory = + collectSessionHistory || changes.contains(Change::SessionHistory); + + if (presShell && changes.contains(Change::Resize)) { + didResize = true; + } + + mSessionStoreChild->IncrementalSessionStoreUpdate( + browsingContext, maybeFormData, maybeScroll, mEpoch); + } + + if (collectWireFrame) { + collectSessionHistory = CollectWireframe() || collectSessionHistory; + } + + mSessionStoreChanges.Clear(); + + Maybe<SessionStoreZoom> zoom; + if (didResize) { + GetZoom(mBrowsingContext->Top(), zoom); + } + + mSessionStoreChild->UpdateSessionStore(collectSessionHistory, zoom); +} + +/* static */ +SessionStoreChangeListener* SessionStoreChangeListener::CollectSessionStoreData( + WindowContext* aWindowContext, const EnumSet<Change>& aChanges) { + SessionStoreChild* sessionStoreChild = + SessionStoreChild::From(aWindowContext->GetWindowGlobalChild()); + if (!sessionStoreChild) { + return nullptr; + } + + SessionStoreChangeListener* sessionStoreChangeListener = + sessionStoreChild->GetSessionStoreChangeListener(); + + if (!sessionStoreChangeListener) { + return nullptr; + } + + sessionStoreChangeListener->RecordChange(aWindowContext, aChanges); + + return sessionStoreChangeListener; +} + +void SessionStoreChangeListener::SetActor( + SessionStoreChild* aSessionStoreChild) { + mSessionStoreChild = aSessionStoreChild; +} + +bool SessionStoreChangeListener::CollectWireframe() { + if (auto* docShell = nsDocShell::Cast(mBrowsingContext->GetDocShell())) { + return docShell->CollectWireframe(); + } + + return false; +} + +void SessionStoreChangeListener::RecordChange(WindowContext* aWindowContext, + EnumSet<Change> aChange) { + EnsureTimer(); + + Unused << mSessionStoreChanges.WithEntryHandle( + aWindowContext, [&](auto entryHandle) -> EnumSet<Change>& { + if (entryHandle) { + *entryHandle += aChange; + return *entryHandle; + } + + return entryHandle.Insert(aChange); + }); +} + +SessionStoreChangeListener::SessionStoreChangeListener( + BrowsingContext* aBrowsingContext) + : mBrowsingContext(aBrowsingContext), + mEpoch(aBrowsingContext->GetSessionStoreEpoch()) {} + +void SessionStoreChangeListener::Init() { + AddEventListeners(); + + Preferences::AddStrongObservers(this, kObservedPrefs); +} + +EventTarget* SessionStoreChangeListener::GetEventTarget() { + if (mBrowsingContext->GetDOMWindow()) { + return mBrowsingContext->GetDOMWindow()->GetChromeEventHandler(); + } + + return nullptr; +} + +void SessionStoreChangeListener::AddEventListeners() { + if (EventTarget* target = GetEventTarget()) { + target->AddSystemEventListener(kInput, this, false); + target->AddSystemEventListener(kScroll, this, false); + if (StaticPrefs::browser_sessionstore_collect_zoom_AtStartup()) { + target->AddSystemEventListener(kResize, this, false); + } + mCurrentEventTarget = target; + } +} + +void SessionStoreChangeListener::RemoveEventListeners() { + if (mCurrentEventTarget) { + mCurrentEventTarget->RemoveSystemEventListener(kInput, this, false); + mCurrentEventTarget->RemoveSystemEventListener(kScroll, this, false); + if (StaticPrefs::browser_sessionstore_collect_zoom_AtStartup()) { + mCurrentEventTarget->RemoveSystemEventListener(kResize, this, false); + } + } + + mCurrentEventTarget = nullptr; +} + +void SessionStoreChangeListener::EnsureTimer() { + if (mTimer) { + return; + } + + if (!StaticPrefs::browser_sessionstore_debug_no_auto_updates()) { + auto result = NS_NewTimerWithCallback( + this, StaticPrefs::browser_sessionstore_interval(), + nsITimer::TYPE_ONE_SHOT); + if (result.isErr()) { + return; + } + + mTimer = result.unwrap(); + } +} diff --git a/toolkit/components/sessionstore/SessionStoreChangeListener.h b/toolkit/components/sessionstore/SessionStoreChangeListener.h new file mode 100644 index 0000000000..803a2137bd --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreChangeListener.h @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreChangeListener_h +#define mozilla_dom_SessionStoreChangeListener_h + +#include "ErrorList.h" + +#include "PLDHashTable.h" +#include "mozilla/EnumSet.h" +#include "nsCycleCollectionParticipant.h" +#include "nsHashKeys.h" +#include "nsIDOMEventListener.h" +#include "nsIObserver.h" +#include "nsISupports.h" +#include "nsITimer.h" +#include "nsINamed.h" +#include "nsHashtablesFwd.h" + +#include "mozilla/EnumSet.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" + +#include "mozilla/dom/WindowGlobalChild.h" + +namespace mozilla::dom { + +class BrowsingContext; +class Element; +class EventTarget; +class SessionStoreChild; +class WindowContext; + +class SessionStoreChangeListener final : public nsINamed, + public nsIObserver, + public nsITimerCallback, + public nsIDOMEventListener { + public: + NS_DECL_NSINAMED + NS_DECL_NSIOBSERVER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(SessionStoreChangeListener, + nsIObserver) + + static already_AddRefed<SessionStoreChangeListener> Create( + BrowsingContext* aBrowsingContext); + + void Stop(); + + void UpdateEventTargets(); + + void FlushSessionStore(); + + enum class Change { Input, Scroll, SessionHistory, WireFrame, Resize }; + + static SessionStoreChangeListener* CollectSessionStoreData( + WindowContext* aWindowContext, const EnumSet<Change>& aChanges); + + void SetActor(SessionStoreChild* aSessionStoreChild); + + void SetEpoch(uint32_t aEpoch) { mEpoch = aEpoch; } + + uint32_t GetEpoch() const { return mEpoch; } + + bool CollectWireframe(); + + private: + void RecordChange(WindowContext* aWindowContext, EnumSet<Change> aChanges); + + public: + using SessionStoreChangeTable = + nsTHashMap<RefPtr<WindowContext>, EnumSet<Change>>; + + private: + explicit SessionStoreChangeListener(BrowsingContext* aBrowsingContext); + ~SessionStoreChangeListener() = default; + + void Init(); + + EventTarget* GetEventTarget(); + + void AddEventListeners(); + void RemoveEventListeners(); + + void EnsureTimer(); + + RefPtr<BrowsingContext> mBrowsingContext; + RefPtr<EventTarget> mCurrentEventTarget; + + uint32_t mEpoch; + nsCOMPtr<nsITimer> mTimer; + RefPtr<SessionStoreChild> mSessionStoreChild; + SessionStoreChangeTable mSessionStoreChanges; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SessionStoreChangeListener_h diff --git a/toolkit/components/sessionstore/SessionStoreChild.cpp b/toolkit/components/sessionstore/SessionStoreChild.cpp new file mode 100644 index 0000000000..6ffd74d9bd --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreChild.cpp @@ -0,0 +1,267 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/SessionStoreChild.h" + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/InProcessChild.h" +#include "mozilla/dom/InProcessParent.h" +#include "mozilla/dom/BrowserSessionStore.h" +#include "mozilla/dom/SessionStoreChangeListener.h" +#include "mozilla/dom/SessionStoreParent.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/ipc/Endpoint.h" +#include "nsCOMPtr.h" +#include "nsFrameLoader.h" + +using namespace mozilla; +using namespace mozilla::dom; + +class nsIDocShell; + +static already_AddRefed<TabListener> CreateTabListener(nsIDocShell* aDocShell) { + RefPtr<TabListener> tabListener = + mozilla::MakeRefPtr<TabListener>(aDocShell, nullptr); + nsresult rv = tabListener->Init(); + if (NS_FAILED(rv)) { + return nullptr; + } + + return tabListener.forget(); +} + +already_AddRefed<SessionStoreChild> SessionStoreChild::GetOrCreate( + BrowsingContext* aBrowsingContext, Element* aOwnerElement) { + RefPtr<TabListener> tabListener = + CreateTabListener(aBrowsingContext->GetDocShell()); + if (!tabListener) { + return nullptr; + } + + RefPtr<SessionStoreChangeListener> sessionStoreChangeListener = + SessionStoreChangeListener::Create(aBrowsingContext); + if (!sessionStoreChangeListener) { + return nullptr; + } + + RefPtr<SessionStoreChild> sessionStoreChild = + new SessionStoreChild(tabListener, sessionStoreChangeListener); + + sessionStoreChangeListener->SetActor(sessionStoreChild); + + if (XRE_IsParentProcess()) { + MOZ_DIAGNOSTIC_ASSERT(aOwnerElement); + InProcessChild* inProcessChild = InProcessChild::Singleton(); + InProcessParent* inProcessParent = InProcessParent::Singleton(); + if (!inProcessChild || !inProcessParent) { + return nullptr; + } + + RefPtr<BrowserSessionStore> sessionStore = + BrowserSessionStore::GetOrCreate(aBrowsingContext->Canonical()->Top()); + if (!sessionStore) { + return nullptr; + } + + CanonicalBrowsingContext* browsingContext = aBrowsingContext->Canonical(); + RefPtr<SessionStoreParent> sessionStoreParent = + new SessionStoreParent(browsingContext, sessionStore); + ManagedEndpoint<PSessionStoreParent> endpoint = + inProcessChild->OpenPSessionStoreEndpoint(sessionStoreChild); + inProcessParent->BindPSessionStoreEndpoint(std::move(endpoint), + sessionStoreParent); + } else { + MOZ_DIAGNOSTIC_ASSERT(!aOwnerElement); + RefPtr<BrowserChild> browserChild = + BrowserChild::GetFrom(aBrowsingContext->GetDOMWindow()); + + MOZ_DIAGNOSTIC_ASSERT(browserChild); + MOZ_DIAGNOSTIC_ASSERT(aBrowsingContext->IsInProcess()); + sessionStoreChild = static_cast<SessionStoreChild*>( + browserChild->SendPSessionStoreConstructor(sessionStoreChild)); + } + + return sessionStoreChild.forget(); +} + +/* static */ +SessionStoreChild* SessionStoreChild::From(WindowGlobalChild* aWindowChild) { + if (!aWindowChild) { + return nullptr; + } + + // If `aWindowChild` is inprocess + if (RefPtr<BrowserChild> browserChild = aWindowChild->GetBrowserChild()) { + return browserChild->GetSessionStoreChild(); + } + + if (XRE_IsContentProcess()) { + return nullptr; + } + + WindowGlobalParent* windowParent = aWindowChild->WindowContext()->Canonical(); + if (!windowParent) { + return nullptr; + } + + RefPtr<nsFrameLoader> frameLoader = windowParent->GetRootFrameLoader(); + if (!frameLoader) { + return nullptr; + } + + return frameLoader->GetSessionStoreChild(); +} + +SessionStoreChild::SessionStoreChild( + TabListener* aSessionStoreListener, + SessionStoreChangeListener* aSessionStoreChangeListener) + : mSessionStoreListener(aSessionStoreListener), + mSessionStoreChangeListener(aSessionStoreChangeListener) {} + +void SessionStoreChild::SetEpoch(uint32_t aEpoch) { + if (mSessionStoreListener) { + mSessionStoreListener->SetEpoch(aEpoch); + } + + if (mSessionStoreChangeListener) { + mSessionStoreChangeListener->SetEpoch(aEpoch); + } +} + +void SessionStoreChild::SetOwnerContent(Element* aElement) { + if (mSessionStoreChangeListener) { + mSessionStoreChangeListener->FlushSessionStore(); + } + + if (!aElement) { + return; + } + + if (mSessionStoreListener) { + mSessionStoreListener->SetOwnerContent(aElement); + } +} + +void SessionStoreChild::Stop() { + if (mSessionStoreListener) { + mSessionStoreListener->RemoveListeners(); + mSessionStoreListener = nullptr; + } + + if (mSessionStoreChangeListener) { + mSessionStoreChangeListener->Stop(); + } +} + +void SessionStoreChild::UpdateEventTargets() { + if (mSessionStoreChangeListener) { + mSessionStoreChangeListener->UpdateEventTargets(); + } +} + +void SessionStoreChild::UpdateSessionStore(bool aSessionHistoryUpdate, + const MaybeSessionStoreZoom& aZoom) { + if (!mSessionStoreListener) { + // This is the case when we're shutting down, and expect a final update. + SessionStoreUpdate(Nothing(), Nothing(), Nothing(), aSessionHistoryUpdate, + 0); + return; + } + + RefPtr<ContentSessionStore> store = mSessionStoreListener->GetSessionStore(); + + Maybe<nsCString> docShellCaps; + if (store->IsDocCapChanged()) { + docShellCaps.emplace(store->GetDocShellCaps()); + } + + Maybe<bool> privatedMode; + if (store->IsPrivateChanged()) { + privatedMode.emplace(store->GetPrivateModeEnabled()); + } + + SessionStoreUpdate( + docShellCaps, privatedMode, aZoom, + store->GetAndClearSHistoryChanged() || aSessionHistoryUpdate, + mSessionStoreListener->GetEpoch()); +} + +void SessionStoreChild::FlushSessionStore() { + if (mSessionStoreChangeListener) { + mSessionStoreChangeListener->FlushSessionStore(); + } +} + +void SessionStoreChild::UpdateSHistoryChanges() { + if (mSessionStoreListener) { + mSessionStoreListener->UpdateSHistoryChanges(); + } +} + +mozilla::ipc::IPCResult SessionStoreChild::RecvFlushTabState( + FlushTabStateResolver&& aResolver) { + if (mSessionStoreChangeListener) { + mSessionStoreChangeListener->FlushSessionStore(); + } + aResolver(true); + + return IPC_OK(); +} + +void SessionStoreChild::SessionStoreUpdate( + const Maybe<nsCString>& aDocShellCaps, const Maybe<bool>& aPrivatedMode, + const MaybeSessionStoreZoom& aZoom, const bool aNeedCollectSHistory, + const uint32_t& aEpoch) { + // Skipping an update here is acceptable, since it will only happen + // during actor teardown, and we're most likely in a final flush + // which expects that not all content processes manage to respond. + if (XRE_IsContentProcess() && CanSend()) { + Unused << SendSessionStoreUpdate(aDocShellCaps, aPrivatedMode, aZoom, + aNeedCollectSHistory, aEpoch); + } else if (SessionStoreParent* sessionStoreParent = + static_cast<SessionStoreParent*>( + InProcessChild::ParentActorFor(this))) { + sessionStoreParent->SessionStoreUpdate(aDocShellCaps, aPrivatedMode, aZoom, + aNeedCollectSHistory, aEpoch); + } +} + +void SessionStoreChild::IncrementalSessionStoreUpdate( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, + const Maybe<FormData>& aFormData, const Maybe<nsPoint>& aScrollPosition, + uint32_t aEpoch) { + // Skipping an update here is acceptable, since it will only happen + // during actor teardown, and we're most likely in a final flush + // which expects that not all content processes manage to respond. + if (XRE_IsContentProcess() && CanSend()) { + Unused << SendIncrementalSessionStoreUpdate(aBrowsingContext, aFormData, + aScrollPosition, aEpoch); + } else if (SessionStoreParent* sessionStoreParent = + static_cast<SessionStoreParent*>( + InProcessChild::ParentActorFor(this))) { + sessionStoreParent->IncrementalSessionStoreUpdate( + aBrowsingContext, aFormData, aScrollPosition, aEpoch); + } +} + +void SessionStoreChild::ResetSessionStore( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, uint32_t aEpoch) { + if (XRE_IsContentProcess()) { + Unused << SendResetSessionStore(aBrowsingContext, aEpoch); + } else if (SessionStoreParent* sessionStoreParent = + static_cast<SessionStoreParent*>( + InProcessChild::ParentActorFor(this))) { + sessionStoreParent->ResetSessionStore(aBrowsingContext, aEpoch); + } +} + +NS_IMPL_CYCLE_COLLECTION(SessionStoreChild, mSessionStoreListener, + mSessionStoreChangeListener) diff --git a/toolkit/components/sessionstore/SessionStoreChild.h b/toolkit/components/sessionstore/SessionStoreChild.h new file mode 100644 index 0000000000..606aa0f441 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreChild.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreChild_h +#define mozilla_dom_SessionStoreChild_h + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/PSessionStoreChild.h" +#include "mozilla/dom/SessionStoreScrollData.h" +#include "mozilla/dom/SessionStoreChangeListener.h" +#include "mozilla/dom/SessionStoreListener.h" +#include "mozilla/RefPtr.h" + +#include "nsCycleCollectionParticipant.h" + +namespace mozilla::dom { +class BrowsingContext; +class SessionStoreChangeListener; +class TabListener; + +class SessionStoreChild final : public PSessionStoreChild { + public: + static already_AddRefed<SessionStoreChild> GetOrCreate( + BrowsingContext* aBrowsingContext, Element* aOwnerElement = nullptr); + + static SessionStoreChild* From(WindowGlobalChild* aWindowChild); + + void SetEpoch(uint32_t aEpoch); + void SetOwnerContent(Element* aElement); + void Stop(); + void UpdateEventTargets(); + void UpdateSessionStore(bool aSessionHistoryUpdate = false, + const MaybeSessionStoreZoom& aZoom = Nothing()); + void FlushSessionStore(); + void UpdateSHistoryChanges(); + + void SessionStoreUpdate(const Maybe<nsCString>& aDocShellCaps, + const Maybe<bool>& aPrivatedMode, + const MaybeSessionStoreZoom& aZoom, + const bool aNeedCollectSHistory, + const uint32_t& aEpoch); + + void IncrementalSessionStoreUpdate( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, + const Maybe<FormData>& aFormData, const Maybe<nsPoint>& aScrollPosition, + uint32_t aEpoch); + + void ResetSessionStore( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, uint32_t aEpoch); + + SessionStoreChangeListener* GetSessionStoreChangeListener() const { + return mSessionStoreChangeListener; + } + + mozilla::ipc::IPCResult RecvFlushTabState(FlushTabStateResolver&& aResolver); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(SessionStoreChild) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(SessionStoreChild) + + private: + SessionStoreChild(TabListener* aSessionStoreListener, + SessionStoreChangeListener* aSessionStoreChangeListener); + ~SessionStoreChild() = default; + + RefPtr<TabListener> mSessionStoreListener; + RefPtr<SessionStoreChangeListener> mSessionStoreChangeListener; +}; +} // namespace mozilla::dom + +#endif // mozilla_dom_SessionStoreChild_h diff --git a/toolkit/components/sessionstore/SessionStoreData.h b/toolkit/components/sessionstore/SessionStoreData.h new file mode 100644 index 0000000000..0df1ea5bf5 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreData.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreData_h +#define mozilla_dom_SessionStoreData_h + +#include "nsString.h" +#include "nsTArray.h" +#include "mozilla/dom/SessionStoreUtilsBinding.h" +#include "mozilla/Variant.h" + +typedef mozilla::Variant<nsString, bool, + mozilla::dom::CollectedNonMultipleSelectValue, + CopyableTArray<nsString>> + InputDataValue; + +/* + * Need two arrays based on this struct. + * One is for elements with id one is for XPath. + * + * id: id or XPath + * type: type of this input element + * bool: value is boolean + * string: value is nsString + * file: value is "arrayVal" + * singleSelect: value is "singleSelect" + * multipleSelect: value is "arrayVal" + * + * There are four value types: + * strVal: nsString + * boolVal: boolean + * singleSelect: single select value + * arrayVal: nsString array + */ +struct CollectedInputDataValue { + nsString id; + nsString type; + InputDataValue value{false}; + + CollectedInputDataValue() = default; +}; + +/* + * Each index of the following array is corresponging to each frame. + * descendants: number of child frames of this frame + * innerHTML: innerHTML of this frame + * url: url of this frame + * numId: number of containing elements with id for this frame + * numXPath: number of containing elements with XPath for this frame + */ +struct InputFormData { + int32_t descendants; + nsString innerHTML; + nsCString url; + int32_t numId; + int32_t numXPath; +}; + +#endif /* mozilla_dom_SessionStoreData_h */ diff --git a/toolkit/components/sessionstore/SessionStoreFormData.cpp b/toolkit/components/sessionstore/SessionStoreFormData.cpp new file mode 100644 index 0000000000..c792b3f2f0 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreFormData.cpp @@ -0,0 +1,172 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/SessionStoreFormData.h" + +#include "mozilla/Assertions.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/BrowserSessionStoreBinding.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/sessionstore/SessionStoreTypes.h" + +#include "nsContentUtils.h" +#include "js/JSON.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(SessionStoreFormData) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SessionStoreFormData) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK_PTR(SessionStoreFormData, mChildren) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SessionStoreFormData) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +nsISupports* SessionStoreFormData::GetParentObject() const { + return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); +} + +JSObject* SessionStoreFormData::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return SessionStoreFormData_Binding::Wrap(aCx, this, aGivenProto); +} + +void SessionStoreFormData::GetUrl(nsACString& aUrl) const { + if (mUrl.IsEmpty()) { + aUrl.SetIsVoid(true); + } else { + aUrl = mUrl; + } +} + +void SessionStoreFormData::GetId( + JSContext* aCx, + Nullable<Record<nsString, OwningStringOrBooleanOrObject>>& aId) { + if (mId.IsEmpty() || + NS_FAILED(SessionStoreUtils::ConstructFormDataValues( + aCx, mId, aId.SetValue().Entries(), mParseSessionData))) { + aId.SetNull(); + } + + // SessionStoreFormData.id is now stored in a slot, so we can free our + // internal state. + mId.Clear(); +} + +void SessionStoreFormData::GetXpath( + JSContext* aCx, + Nullable<Record<nsString, OwningStringOrBooleanOrObject>>& aXpath) { + if (mXpath.IsEmpty() || + NS_FAILED(SessionStoreUtils::ConstructFormDataValues( + aCx, mXpath, aXpath.SetValue().Entries(), mParseSessionData))) { + aXpath.SetNull(); + } + + // SessionStoreFormData.xpath is now stored in a slot, so we can free our + // internal state. + mXpath.Clear(); +} + +void SessionStoreFormData::GetInnerHTML(nsAString& aInnerHTML) { + if (mInnerHTML.IsEmpty()) { + SetDOMStringToNull(aInnerHTML); + } else { + aInnerHTML = mInnerHTML; + } + + // SessionStoreFormData.innerHTML is now stored in a slot, so we can free our + // internal state. + mInnerHTML.SetIsVoid(true); +} + +SessionStoreFormData::ChildrenArray& SessionStoreFormData::Children() { + return mChildren; +} + +void SessionStoreFormData::GetChildren( + Nullable<ChildrenArray>& aChildren) const { + if (!mChildren.IsEmpty()) { + aChildren.SetValue() = mChildren.Clone(); + } else { + aChildren.SetNull(); + } +} + +void SessionStoreFormData::ToJSON(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval) { + JS::Rooted<JSObject*> self(aCx); + { + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, this, &value)) { + return; + } + + self.set(value.toObjectOrNull()); + } + + JS::Rooted<JSObject*> result(aCx, JS_NewPlainObject(aCx)); + + if (!IsEmpty()) { + for (const auto& name : + {u"url"_ns, u"id"_ns, u"xpath"_ns, u"innerHTML"_ns}) { + if (!SessionStoreUtils::CopyProperty(aCx, result, self, name)) { + return; + } + } + + SessionStoreUtils::CopyChildren(aCx, result, mChildren); + } + + aRetval.set(result); +} + +void SessionStoreFormData::Update(const CollectedType& aFormData) { + SessionStoreFormData_Binding::ClearCachedUrlValue(this); + SessionStoreFormData_Binding::ClearCachedIdValue(this); + SessionStoreFormData_Binding::ClearCachedXpathValue(this); + SessionStoreFormData_Binding::ClearCachedInnerHTMLValue(this); + + if (!aFormData.hasData()) { + mParseSessionData = false; + mHasData = false; + mUrl = ""_ns; + mId.Clear(); + mXpath.Clear(); + mInnerHTML = u""_ns; + + return; + } + + mHasData = true; + + mUrl = aFormData.uri(); + // We want to avoid saving data for about:sessionrestore as a string. + // Since it's stored in the form as stringified JSON, stringifying + // further causes an explosion of escape characters. cf. bug 467409 + mParseSessionData = + mUrl == "about:sessionrestore"_ns || mUrl == "about:welcomeback"_ns; + + mInnerHTML = aFormData.innerHTML(); + + mId.Assign(aFormData.id()); + mXpath.Assign(aFormData.xpath()); +} + +void SessionStoreFormData::ClearCachedChildren() { + SessionStoreFormData_Binding::ClearCachedChildrenValue(this); +} + +/* static */ bool SessionStoreFormData::HasData( + const CollectedType& aFormData) { + return aFormData.hasData(); +} + +bool SessionStoreFormData::IsEmpty() const { + return !mHasData && mChildren.IsEmpty(); +} diff --git a/toolkit/components/sessionstore/SessionStoreFormData.h b/toolkit/components/sessionstore/SessionStoreFormData.h new file mode 100644 index 0000000000..0466d98441 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreFormData.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreFormData_h +#define mozilla_dom_SessionStoreFormData_h + +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/WindowGlobalParent.h" + +#include "nsTArrayForwardDeclare.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla::dom { + +namespace sessionstore { +class FormData; +} + +class BrowsingContext; +class OwningByteStringOrObjectOrNull; +class OwningStringOrObjectOrNull; +class WindowGlobalParent; + +class SessionStoreFormData final : public nsISupports, + public nsWrapperCache, + public SupportsWeakPtr { + public: + using CollectedType = sessionstore::FormData; + using LocationType = WeakPtr<SessionStoreFormData>; + using ChildrenArray = nsTArray<RefPtr<SessionStoreFormData>>; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SessionStoreFormData) + nsISupports* GetParentObject() const; + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetUrl(nsACString& aUrl) const; + + void GetId(JSContext* aCx, + Nullable<Record<nsString, OwningStringOrBooleanOrObject>>& aId); + + void GetXpath( + JSContext* aCx, + Nullable<Record<nsString, OwningStringOrBooleanOrObject>>& aXpath); + + void GetInnerHTML(nsAString& aInnerHTML); + + ChildrenArray& Children(); + + void GetChildren(Nullable<ChildrenArray>& aChildren) const; + + void ToJSON(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval); + + void Update(const CollectedType& aFormData); + + void ClearCachedChildren(); + + static bool HasData(const CollectedType& aFormData); + + bool IsEmpty() const; + + private: + ~SessionStoreFormData() = default; + + bool mParseSessionData = false; + bool mHasData = false; + nsCString mUrl; + nsTArray<sessionstore::FormEntry> mId; + nsTArray<sessionstore::FormEntry> mXpath; + nsString mInnerHTML; + ChildrenArray mChildren; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SessionStoreFormData_h diff --git a/toolkit/components/sessionstore/SessionStoreFunctions.idl b/toolkit/components/sessionstore/SessionStoreFunctions.idl new file mode 100644 index 0000000000..3c17015437 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreFunctions.idl @@ -0,0 +1,23 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl BrowsingContext; +webidl Element; + +[scriptable, uuid(1A060FBA-A19D-11E9-B7EB-580D0EDD8E6F)] +interface nsISessionStoreFunctions : nsISupports { + // update sessionStore from the tabListener implemented by C++ + // aData is a UpdateSessionStoreData dictionary (From SessionStoreUtils.webidl) + void UpdateSessionStore( + in Element aBrowser, in BrowsingContext aBrowsingContext, + in jsval aPermanentKey, in uint32_t aEpoch, in boolean aCollectSHistory, + in jsval aData); + + void UpdateSessionStoreForStorage( + in Element aBrowser, in BrowsingContext aBrowsingContext, + in jsval aPermanentKey, in uint32_t aEpoch, in jsval aData); +}; diff --git a/toolkit/components/sessionstore/SessionStoreFunctions.sys.mjs b/toolkit/components/sessionstore/SessionStoreFunctions.sys.mjs new file mode 100644 index 0000000000..f42c371264 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreFunctions.sys.mjs @@ -0,0 +1,87 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { SessionStore } from "resource:///modules/sessionstore/SessionStore.sys.mjs"; + +export function UpdateSessionStore( + aBrowser, + aBrowsingContext, + aPermanentKey, + aEpoch, + aCollectSHistory, + aData +) { + return SessionStoreFuncInternal.updateSessionStore( + aBrowser, + aBrowsingContext, + aPermanentKey, + aEpoch, + aCollectSHistory, + aData + ); +} + +export function UpdateSessionStoreForStorage( + aBrowser, + aBrowsingContext, + aPermanentKey, + aEpoch, + aData +) { + return SessionStoreFuncInternal.updateSessionStoreForStorage( + aBrowser, + aBrowsingContext, + aPermanentKey, + aEpoch, + aData + ); +} + +var SessionStoreFuncInternal = { + updateSessionStore: function SSF_updateSessionStore( + aBrowser, + aBrowsingContext, + aPermanentKey, + aEpoch, + aCollectSHistory, + aData + ) { + let { formdata, scroll } = aData; + + if (formdata) { + aData.formdata = formdata.toJSON(); + } + + if (scroll) { + aData.scroll = scroll.toJSON(); + } + + SessionStore.updateSessionStoreFromTablistener( + aBrowser, + aBrowsingContext, + aPermanentKey, + { + data: aData, + epoch: aEpoch, + sHistoryNeeded: aCollectSHistory, + } + ); + }, + + updateSessionStoreForStorage: function SSF_updateSessionStoreForStorage( + aBrowser, + aBrowsingContext, + aPermanentKey, + aEpoch, + aData + ) { + SessionStore.updateSessionStoreFromTablistener( + aBrowser, + aBrowsingContext, + aPermanentKey, + { data: { storage: aData }, epoch: aEpoch }, + true + ); + }, +}; diff --git a/toolkit/components/sessionstore/SessionStoreListener.cpp b/toolkit/components/sessionstore/SessionStoreListener.cpp new file mode 100644 index 0000000000..11b9966954 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreListener.cpp @@ -0,0 +1,493 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/PresShell.h" +#include "mozilla/dom/BrowserSessionStoreBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/SessionStoreListener.h" +#include "mozilla/dom/SessionStoreUtils.h" +#include "mozilla/dom/SessionStoreUtilsBinding.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/StaticPrefs_browser.h" +#include "nsGenericHTMLElement.h" +#include "nsDocShell.h" +#include "nsIAppWindow.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeOwner.h" +#include "nsImportModule.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsITimer.h" +#include "nsIWebProgress.h" +#include "nsIXPConnect.h" +#include "nsIXULRuntime.h" +#include "nsPresContext.h" +#include "nsPrintfCString.h" +#include "SessionStoreFunctions.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// This pref controls whether or not we send updates to the parent on a timeout +// or not, and should only be used for tests or debugging. +static const char kTimeOutDisable[] = + "browser.sessionstore.debug.no_auto_updates"; +// Timeout for waiting an idle period to send data. +static const char kPrefInterval[] = "browser.sessionstore.interval"; + +NS_IMPL_CYCLE_COLLECTION(ContentSessionStore, mDocShell) + +ContentSessionStore::ContentSessionStore(nsIDocShell* aDocShell) + : mDocShell(aDocShell), + mPrivateChanged(false), + mIsPrivate(false), + mDocCapChanged(false), + mSHistoryChanged(false) { + MOZ_ASSERT(mDocShell); + // Check that value at startup as it might have + // been set before the frame script was loaded. + if (NS_SUCCEEDED(nsDocShell::Cast(mDocShell)->GetUsePrivateBrowsing( + &mPrivateChanged)) && + mPrivateChanged) { + mIsPrivate = true; + } +} + +nsCString ContentSessionStore::CollectDocShellCapabilities() { + bool allow; + nsCString aRetVal; + +#define TRY_ALLOWPROP(y) \ + PR_BEGIN_MACRO \ + nsresult rv = mDocShell->GetAllow##y(&allow); \ + if (NS_SUCCEEDED(rv) && !allow) { \ + if (!aRetVal.IsEmpty()) { \ + aRetVal.Append(','); \ + } \ + aRetVal.Append(#y); \ + } \ + PR_END_MACRO + + // Bug 1328013 : Don't collect "AllowJavascript" property + // TRY_ALLOWPROP(Javascript); + TRY_ALLOWPROP(MetaRedirects); + TRY_ALLOWPROP(Subframes); + TRY_ALLOWPROP(Images); + TRY_ALLOWPROP(Media); + TRY_ALLOWPROP(DNSPrefetch); + TRY_ALLOWPROP(WindowControl); + TRY_ALLOWPROP(Auth); + TRY_ALLOWPROP(ContentRetargeting); + TRY_ALLOWPROP(ContentRetargetingOnChildren); +#undef TRY_ALLOWPROP + return aRetVal; +} + +void ContentSessionStore::OnPrivateModeChanged(bool aEnabled) { + mPrivateChanged = true; + mIsPrivate = aEnabled; +} + +nsCString ContentSessionStore::GetDocShellCaps() { + mDocCapChanged = false; + return mDocCaps; +} + +bool ContentSessionStore::GetPrivateModeEnabled() { + mPrivateChanged = false; + return mIsPrivate; +} + +void ContentSessionStore::SetSHistoryChanged() { + mSHistoryChanged = mozilla::SessionHistoryInParent(); +} + +void ContentSessionStore::OnDocumentStart() { + nsCString caps = CollectDocShellCapabilities(); + if (!mDocCaps.Equals(caps)) { + mDocCaps = caps; + mDocCapChanged = true; + } + + if (mozilla::SessionHistoryInParent()) { + mSHistoryChanged = true; + } +} + +void ContentSessionStore::OnDocumentEnd() { + if (mozilla::SessionHistoryInParent()) { + mSHistoryChanged = true; + } +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TabListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIPrivacyTransitionObserver) + NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WEAK(TabListener, mDocShell, mSessionStore, + mOwnerContent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TabListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TabListener) + +TabListener::TabListener(nsIDocShell* aDocShell, Element* aElement) + : mDocShell(aDocShell), + mSessionStore(new ContentSessionStore(aDocShell)), + mOwnerContent(aElement), + mProgressListenerRegistered(false), + mEventListenerRegistered(false), + mPrefObserverRegistered(false), + mUpdatedTimer(nullptr), + mTimeoutDisabled(false), + mUpdateInterval(15000), + mEpoch(0) { + MOZ_ASSERT(mDocShell); +} + +EventTarget* TabListener::GetEventTarget() { + if (mOwnerContent) { + return mOwnerContent; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = do_GetInterface(mDocShell); + if (window) { + return window->GetChromeEventHandler(); + } + + return nullptr; +} + +nsresult TabListener::Init() { + TabListener::UpdateSessionStore(); + nsresult rv = mDocShell->AddWeakPrivacyTransitionObserver(this); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIWebProgress> webProgress = do_QueryInterface(mDocShell); + rv = webProgress->AddProgressListener(this, + nsIWebProgress::NOTIFY_STATE_DOCUMENT); + NS_ENSURE_SUCCESS(rv, rv); + mProgressListenerRegistered = true; + + nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); + NS_WARNING_ASSERTION(prefBranch, "no prefservice"); + if (prefBranch) { + prefBranch->AddObserver(kTimeOutDisable, this, true); + prefBranch->AddObserver(kPrefInterval, this, true); + mPrefObserverRegistered = true; + } + + AddEventListeners(); + return NS_OK; +} + +void TabListener::AddEventListeners() { + if (nsCOMPtr<EventTarget> eventTarget = GetEventTarget()) { + if (mozilla::SessionHistoryInParent()) { + eventTarget->AddSystemEventListener(u"DOMTitleChanged"_ns, this, false); + } + mEventListenerRegistered = true; + } +} + +void TabListener::RemoveEventListeners() { + if (nsCOMPtr<EventTarget> eventTarget = GetEventTarget()) { + if (mEventListenerRegistered) { + if (mozilla::SessionHistoryInParent()) { + eventTarget->RemoveSystemEventListener(u"DOMTitleChanged"_ns, this, + false); + } + mEventListenerRegistered = false; + } + } +} + +void TabListener::SetOwnerContent(Element* aElement) { + MOZ_DIAGNOSTIC_ASSERT(aElement); + RemoveEventListeners(); + mOwnerContent = aElement; + AddEventListeners(); +} + +/* static */ +void TabListener::TimerCallback(nsITimer* aTimer, void* aClosure) { + auto listener = static_cast<TabListener*>(aClosure); + listener->UpdateSessionStore(); + listener->StopTimerForUpdate(); +} + +void TabListener::StopTimerForUpdate() { + if (mUpdatedTimer) { + mUpdatedTimer->Cancel(); + mUpdatedTimer = nullptr; + } +} + +void TabListener::AddTimerForUpdate() { + if (mUpdatedTimer) { + return; + } + + if (mTimeoutDisabled) { + UpdateSessionStore(); + return; + } + + NS_NewTimerWithFuncCallback(getter_AddRefs(mUpdatedTimer), TimerCallback, + this, mUpdateInterval, nsITimer::TYPE_ONE_SHOT, + "TabListener::TimerCallback"); +} + +NS_IMETHODIMP TabListener::PrivateModeChanged(bool aEnabled) { + mSessionStore->OnPrivateModeChanged(aEnabled); + AddTimerForUpdate(); + return NS_OK; +} + +NS_IMETHODIMP TabListener::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aStateFlags, + nsresult aStatus) { + if (!mSessionStore) { + return NS_OK; + } + + // Ignore state changes for subframes because we're only interested in the + // top-document starting or stopping its load. + bool isTopLevel = false; + nsresult rv = aWebProgress->GetIsTopLevel(&isTopLevel); + NS_ENSURE_SUCCESS(rv, rv); + if (!isTopLevel) { + return NS_OK; + } + + nsCOMPtr<nsIWebProgress> webProgress = do_QueryInterface(mDocShell); + if (webProgress != aWebProgress) { + return NS_OK; + } + + // onStateChange will be fired when loading the initial about:blank URI for + // a browser, which we don't actually care about. This is particularly for + // the case of unrestored background tabs, where the content has not yet + // been restored: we don't want to accidentally send any updates to the + // parent when the about:blank placeholder page has loaded. + if (!mDocShell->GetHasLoadedNonBlankURI()) { + return NS_OK; + } + + if (aStateFlags & (nsIWebProgressListener::STATE_START)) { + mSessionStore->OnDocumentStart(); + } else if (aStateFlags & (nsIWebProgressListener::STATE_STOP)) { + mSessionStore->OnDocumentEnd(); + } + + return NS_OK; +} + +NS_IMETHODIMP +TabListener::HandleEvent(Event* aEvent) { + EventTarget* target = aEvent->GetTarget(); + if (!target) { + return NS_OK; + } + + nsPIDOMWindowOuter* outer = target->GetOwnerGlobalForBindingsInternal(); + if (!outer || !outer->GetDocShell()) { + return NS_OK; + } + + RefPtr<BrowsingContext> context = outer->GetBrowsingContext(); + if (!context || context->CreatedDynamically()) { + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("DOMTitleChanged")) { + mSessionStore->SetSHistoryChanged(); + AddTimerForUpdate(); + } + + return NS_OK; +} + +NS_IMETHODIMP TabListener::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP TabListener::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI* aLocation, + uint32_t aFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP TabListener::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP TabListener::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aState) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP TabListener::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult TabListener::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject); + + bool timeoutDisabled; + if (NS_SUCCEEDED( + prefBranch->GetBoolPref(kTimeOutDisable, &timeoutDisabled))) { + if (mTimeoutDisabled != timeoutDisabled) { + mTimeoutDisabled = timeoutDisabled; + if (mUpdatedTimer) { + StopTimerForUpdate(); + AddTimerForUpdate(); + } + } + } + + int32_t interval = 0; + if (NS_SUCCEEDED(prefBranch->GetIntPref(kPrefInterval, &interval))) { + if (mUpdateInterval != interval) { + mUpdateInterval = interval; + if (mUpdatedTimer) { + StopTimerForUpdate(); + AddTimerForUpdate(); + } + } + } + return NS_OK; + } + + NS_ERROR("Unexpected topic"); + return NS_ERROR_UNEXPECTED; +} + +void TabListener::ForceFlushFromParent() { + if (!XRE_IsParentProcess()) { + return; + } + if (!mSessionStore) { + return; + } + + UpdateSessionStore(true); +} + +void TabListener::UpdateSessionStore(bool aIsFlush) { + if (!aIsFlush) { + if (!mSessionStore || !mSessionStore->UpdateNeeded()) { + return; + } + } + + if (!XRE_IsParentProcess()) { + BrowserChild* browserChild = BrowserChild::GetFrom(mDocShell); + if (browserChild) { + StopTimerForUpdate(); + browserChild->UpdateSessionStore(); + } + return; + } + + BrowsingContext* context = mDocShell->GetBrowsingContext(); + if (!context) { + return; + } + + uint32_t chromeFlags = 0; + nsCOMPtr<nsIDocShellTreeOwner> treeOwner; + mDocShell->GetTreeOwner(getter_AddRefs(treeOwner)); + if (!treeOwner) { + return; + } + nsCOMPtr<nsIAppWindow> window(do_GetInterface(treeOwner)); + if (!window) { + return; + } + if (window && NS_FAILED(window->GetChromeFlags(&chromeFlags))) { + return; + } + + UpdateSessionStoreData data; + if (mSessionStore->IsDocCapChanged()) { + data.mDisallow.Construct() = mSessionStore->GetDocShellCaps(); + } + if (mSessionStore->IsPrivateChanged()) { + data.mIsPrivate.Construct() = mSessionStore->GetPrivateModeEnabled(); + } + + nsCOMPtr<nsISessionStoreFunctions> funcs = do_ImportESModule( + "resource://gre/modules/SessionStoreFunctions.sys.mjs", fallible); + nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(funcs); + if (!wrapped) { + return; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(wrapped->GetJSObjectGlobal())) { + return; + } + + JS::Rooted<JS::Value> update(jsapi.cx()); + if (!ToJSValue(jsapi.cx(), data, &update)) { + return; + } + + JS::Rooted<JS::Value> key(jsapi.cx(), + context->Canonical()->Top()->PermanentKey()); + + nsresult rv = funcs->UpdateSessionStore( + mOwnerContent, context, key, mEpoch, + mSessionStore->GetAndClearSHistoryChanged(), update); + if (NS_FAILED(rv)) { + return; + } + + StopTimerForUpdate(); +} + +void TabListener::RemoveListeners() { + if (mProgressListenerRegistered) { + nsCOMPtr<nsIWebProgress> webProgress = do_QueryInterface(mDocShell); + if (webProgress) { + webProgress->RemoveProgressListener(this); + mProgressListenerRegistered = false; + } + } + + RemoveEventListeners(); + + if (mPrefObserverRegistered) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + return; + } + if (mPrefObserverRegistered) { + obs->RemoveObserver(this, kTimeOutDisable); + obs->RemoveObserver(this, kPrefInterval); + mPrefObserverRegistered = false; + } + } +} + +TabListener::~TabListener() { RemoveListeners(); } diff --git a/toolkit/components/sessionstore/SessionStoreListener.h b/toolkit/components/sessionstore/SessionStoreListener.h new file mode 100644 index 0000000000..2cd35a00f5 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreListener.h @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreListener_h +#define mozilla_dom_SessionStoreListener_h + +#include "SessionStoreData.h" +#include "nsIDOMEventListener.h" +#include "nsIObserver.h" +#include "nsIPrivacyTransitionObserver.h" +#include "nsIWebProgressListener.h" +#include "nsWeakReference.h" + +class nsITimer; + +namespace mozilla::dom { + +class ContentSessionStore { + public: + explicit ContentSessionStore(nsIDocShell* aDocShell); + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ContentSessionStore) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(ContentSessionStore) + + void OnPrivateModeChanged(bool aEnabled); + bool IsDocCapChanged() { return mDocCapChanged; } + nsCString GetDocShellCaps(); + bool IsPrivateChanged() { return mPrivateChanged; } + bool GetPrivateModeEnabled(); + + void SetSHistoryChanged(); + // request "collect sessionHistory" which is happened in the parent process + bool GetAndClearSHistoryChanged() { + bool ret = mSHistoryChanged; + mSHistoryChanged = false; + return ret; + } + + void OnDocumentStart(); + void OnDocumentEnd(); + bool UpdateNeeded() { + return mPrivateChanged || mDocCapChanged || mSHistoryChanged; + } + + private: + virtual ~ContentSessionStore() = default; + nsCString CollectDocShellCapabilities(); + + nsCOMPtr<nsIDocShell> mDocShell; + bool mPrivateChanged; + bool mIsPrivate; + bool mDocCapChanged; + nsCString mDocCaps; + // mSHistoryChanged means there are history changes which are found + // in the child process. The flag is set when + // 1. webProgress changes to STATE_START + // 2. webProgress changes to STATE_STOP + // 3. receiving "DOMTitleChanged" event + bool mSHistoryChanged; +}; + +class TabListener : public nsIDOMEventListener, + public nsIObserver, + public nsIPrivacyTransitionObserver, + public nsIWebProgressListener, + public nsSupportsWeakReference { + public: + explicit TabListener(nsIDocShell* aDocShell, Element* aElement); + EventTarget* GetEventTarget(); + nsresult Init(); + ContentSessionStore* GetSessionStore() { return mSessionStore; } + // the function is called only when TabListener is in parent process + void ForceFlushFromParent(); + void RemoveListeners(); + void SetEpoch(uint32_t aEpoch) { mEpoch = aEpoch; } + uint32_t GetEpoch() { return mEpoch; } + void UpdateSHistoryChanges() { AddTimerForUpdate(); } + void SetOwnerContent(Element* aElement); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TabListener, nsIDOMEventListener) + + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_NSIOBSERVER + NS_DECL_NSIPRIVACYTRANSITIONOBSERVER + NS_DECL_NSIWEBPROGRESSLISTENER + + private: + static void TimerCallback(nsITimer* aTimer, void* aClosure); + void AddTimerForUpdate(); + void StopTimerForUpdate(); + void AddEventListeners(); + void RemoveEventListeners(); + void UpdateSessionStore(bool aIsFlush = false); + virtual ~TabListener(); + + nsCOMPtr<nsIDocShell> mDocShell; + RefPtr<ContentSessionStore> mSessionStore; + RefPtr<mozilla::dom::Element> mOwnerContent; + bool mProgressListenerRegistered; + bool mEventListenerRegistered; + bool mPrefObserverRegistered; + // Timer used to update data + nsCOMPtr<nsITimer> mUpdatedTimer; + bool mTimeoutDisabled; + int32_t mUpdateInterval; + uint32_t mEpoch; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SessionStoreListener_h diff --git a/toolkit/components/sessionstore/SessionStoreMessageUtils.h b/toolkit/components/sessionstore/SessionStoreMessageUtils.h new file mode 100644 index 0000000000..8475955f87 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreMessageUtils.h @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreMessageUtils_h +#define mozilla_dom_SessionStoreMessageUtils_h + +#include "ipc/IPCMessageUtils.h" +#include "mozilla/ipc/IPDLParamTraits.h" +#include "mozilla/ipc/URIUtils.h" +#include "SessionStoreData.h" +#include "SessionStoreUtils.h" +#include "SessionStoreRestoreData.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::CollectedNonMultipleSelectValue> { + typedef mozilla::dom::CollectedNonMultipleSelectValue paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mSelectedIndex); + WriteParam(aWriter, aParam.mValue); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mSelectedIndex) && + ReadParam(aReader, &aResult->mValue); + } +}; + +template <> +struct ParamTraits<CollectedInputDataValue> { + typedef CollectedInputDataValue paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.id); + WriteParam(aWriter, aParam.type); + WriteParam(aWriter, aParam.value); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->id) && + ReadParam(aReader, &aResult->type) && + ReadParam(aReader, &aResult->value); + } +}; + +template <> +struct ParamTraits<InputFormData> { + typedef InputFormData paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.descendants); + WriteParam(aWriter, aParam.innerHTML); + WriteParam(aWriter, aParam.url); + WriteParam(aWriter, aParam.numId); + WriteParam(aWriter, aParam.numXPath); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->descendants) && + ReadParam(aReader, &aResult->innerHTML) && + ReadParam(aReader, &aResult->url) && + ReadParam(aReader, &aResult->numId) && + ReadParam(aReader, &aResult->numXPath); + } +}; + +} // namespace IPC + +namespace mozilla { +namespace ipc { + +template <> +struct IPDLParamTraits<mozilla::dom::SessionStoreRestoreData*> { + // Note that we intentionally don't de/serialize mChildren here. The receiver + // won't be doing anything with the children lists, and it avoids sending form + // data for subframes to the content processes of their embedders. + + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + mozilla::dom::SessionStoreRestoreData* aParam) { + bool isNull = !aParam; + WriteIPDLParam(aWriter, aActor, isNull); + if (isNull) { + return; + } + WriteIPDLParam(aWriter, aActor, aParam->mURI); + WriteIPDLParam(aWriter, aActor, aParam->mInnerHTML); + WriteIPDLParam(aWriter, aActor, aParam->mScroll); + WriteIPDLParam(aWriter, aActor, aParam->mEntries); + } + + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + RefPtr<mozilla::dom::SessionStoreRestoreData>* aResult) { + *aResult = nullptr; + bool isNull; + if (!ReadIPDLParam(aReader, aActor, &isNull)) { + return false; + } + if (isNull) { + return true; + } + auto data = MakeRefPtr<mozilla::dom::SessionStoreRestoreData>(); + if (!ReadIPDLParam(aReader, aActor, &data->mURI) || + !ReadIPDLParam(aReader, aActor, &data->mInnerHTML) || + !ReadIPDLParam(aReader, aActor, &data->mScroll) || + !ReadIPDLParam(aReader, aActor, &data->mEntries)) { + return false; + } + *aResult = std::move(data); + return true; + } +}; + +template <> +struct IPDLParamTraits<mozilla::dom::SessionStoreRestoreData::Entry> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + mozilla::dom::SessionStoreRestoreData::Entry aParam) { + WriteIPDLParam(aWriter, aActor, aParam.mData); + WriteIPDLParam(aWriter, aActor, aParam.mIsXPath); + } + + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + mozilla::dom::SessionStoreRestoreData::Entry* aResult) { + return ReadIPDLParam(aReader, aActor, &aResult->mData) && + ReadIPDLParam(aReader, aActor, &aResult->mIsXPath); + } +}; + +} // namespace ipc +} // namespace mozilla + +#endif // mozilla_dom_SessionStoreMessageUtils_h diff --git a/toolkit/components/sessionstore/SessionStoreParent.cpp b/toolkit/components/sessionstore/SessionStoreParent.cpp new file mode 100644 index 0000000000..58be84d4d9 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreParent.cpp @@ -0,0 +1,318 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/SessionStoreParent.h" + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/BrowserSessionStore.h" +#include "mozilla/dom/BrowserSessionStoreBinding.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/InProcessChild.h" +#include "mozilla/dom/InProcessParent.h" +#include "mozilla/dom/SessionStoreChild.h" +#include "mozilla/dom/SessionStoreUtilsBinding.h" +#include "SessionStoreFunctions.h" +#include "nsISupports.h" +#include "nsIXULRuntime.h" +#include "nsImportModule.h" +#include "nsIXPConnect.h" + +#ifdef ANDROID +# include "mozilla/widget/nsWindow.h" +# include "mozilla/jni/GeckoBundleUtils.h" +# include "JavaBuiltins.h" +#endif /* ANDROID */ + +using namespace mozilla; +using namespace mozilla::dom; + +SessionStoreParent::SessionStoreParent( + CanonicalBrowsingContext* aBrowsingContext, + BrowserSessionStore* aSessionStore) + : mBrowsingContext(aBrowsingContext), mSessionStore(aSessionStore) {} + +#ifdef ANDROID +static void DoSessionStoreUpdate(CanonicalBrowsingContext* aBrowsingContext, + const Maybe<nsCString>& aDocShellCaps, + const Maybe<bool>& aPrivatedMode, + SessionStoreFormData* aFormData, + SessionStoreScrollData* aScroll, + const MaybeSessionStoreZoom& aZoom, + bool aNeedCollectSHistory, uint32_t aEpoch) { + RefPtr<BrowserSessionStore> sessionStore = + BrowserSessionStore::GetOrCreate(aBrowsingContext->Top()); + + nsCOMPtr<nsIWidget> widget = + aBrowsingContext->GetParentProcessWidgetContaining(); + if (RefPtr<nsWindow> window = nsWindow::From(widget)) { + AutoJSAPI jsapi; + if (!jsapi.Init(xpc::PrivilegedJunkScope())) { + return; + } + jni::Object::LocalRef formDataBundle(jni::GetGeckoThreadEnv()); + jni::Object::LocalRef scrollBundle(jni::GetGeckoThreadEnv()); + + if (aFormData) { + JS::Rooted<JSObject*> object(jsapi.cx()); + ErrorResult rv; + aFormData->ToJSON(jsapi.cx(), &object); + + JS::Rooted<JS::Value> value(jsapi.cx(), JS::ObjectValue(*object)); + + if (NS_FAILED(jni::BoxData(jsapi.cx(), value, formDataBundle, true))) { + JS_ClearPendingException(jsapi.cx()); + return; + } + } + + if (aScroll) { + JS::Rooted<JSObject*> object(jsapi.cx()); + ErrorResult rv; + aScroll->ToJSON(jsapi.cx(), &object); + JS::Rooted<JS::Value> value(jsapi.cx(), JS::ObjectValue(*object)); + + if (NS_FAILED(jni::BoxData(jsapi.cx(), value, scrollBundle, true))) { + JS_ClearPendingException(jsapi.cx()); + return; + } + } + + GECKOBUNDLE_START(update); + GECKOBUNDLE_PUT(update, "formdata", formDataBundle); + GECKOBUNDLE_PUT(update, "scroll", scrollBundle); + if (aZoom) { + GECKOBUNDLE_START(zoomBundle); + GECKOBUNDLE_PUT(zoomBundle, "resolution", + java::sdk::Double::New(std::get<0>(*aZoom))); + GECKOBUNDLE_START(displaySizeBundle); + GECKOBUNDLE_PUT(displaySizeBundle, "width", + java::sdk::Integer::ValueOf(std::get<1>(*aZoom))); + GECKOBUNDLE_PUT(displaySizeBundle, "height", + java::sdk::Integer::ValueOf(std::get<2>(*aZoom))); + GECKOBUNDLE_FINISH(displaySizeBundle); + GECKOBUNDLE_PUT(zoomBundle, "displaySize", displaySizeBundle); + GECKOBUNDLE_FINISH(zoomBundle); + GECKOBUNDLE_PUT(update, "zoom", zoomBundle); + } + GECKOBUNDLE_FINISH(update); + + window->OnUpdateSessionStore(update); + } +} +#else +static void DoSessionStoreUpdate(CanonicalBrowsingContext* aBrowsingContext, + const Maybe<nsCString>& aDocShellCaps, + const Maybe<bool>& aPrivatedMode, + SessionStoreFormData* aFormData, + SessionStoreScrollData* aScroll, + const MaybeSessionStoreZoom& aZoom, + bool aNeedCollectSHistory, uint32_t aEpoch) { + UpdateSessionStoreData data; + if (aDocShellCaps.isSome()) { + auto& disallow = data.mDisallow.Construct(); + if (!aDocShellCaps->IsEmpty()) { + disallow = aDocShellCaps.value(); + } else { + disallow.SetIsVoid(true); + } + } + + if (aPrivatedMode.isSome()) { + data.mIsPrivate.Construct() = aPrivatedMode.value(); + } + + RefPtr<BrowserSessionStore> sessionStore = + BrowserSessionStore::GetOrCreate(aBrowsingContext->Top()); + + if (!aFormData) { + SessionStoreFormData* formData = sessionStore->GetFormdata(); + data.mFormdata.Construct(formData); + } else { + data.mFormdata.Construct(aFormData); + } + + if (!aScroll) { + SessionStoreScrollData* scroll = sessionStore->GetScroll(); + data.mScroll.Construct(scroll); + } else { + data.mScroll.Construct(aScroll); + } + + nsCOMPtr<nsISessionStoreFunctions> funcs = do_ImportESModule( + "resource://gre/modules/SessionStoreFunctions.sys.mjs", fallible); + nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(funcs); + if (!wrapped) { + return; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(wrapped->GetJSObjectGlobal())) { + return; + } + + JS::Rooted<JS::Value> update(jsapi.cx()); + if (!ToJSValue(jsapi.cx(), data, &update)) { + return; + } + + JS::Rooted<JS::Value> key(jsapi.cx(), + aBrowsingContext->Top()->PermanentKey()); + + Unused << funcs->UpdateSessionStore(nullptr, aBrowsingContext, key, aEpoch, + aNeedCollectSHistory, update); +} +#endif + +void SessionStoreParent::FlushAllSessionStoreChildren( + const std::function<void()>& aDone) { + if (!mBrowsingContext) { + aDone(); + return; + } + + nsTArray<RefPtr<SessionStoreParent::FlushTabStatePromise>> flushPromises; + + // We're special casing this for when the SessionStore{Child, Parent} have + // been created in the same process. This is only ever true for the parent + // process session store actor, and is needed because + // nsFrameLoader::RequestTabStateFlush expect flushes to happen faster than we + // can manage by using the common path of sending a message the + // SessionStoreChild. Ideally we should be able to do just that, but not + // without more work. + if (InProcessParent::ChildActorFor(this)) { + // Here we assume that the session store data collection only collect for in + // (parent-)process content type browsing contexts, which we only flush one + // session store actor. + flushPromises.AppendElement(FlushSessionStore()); + } else { + // While here we flush all participating actors. + BrowserParent* browserParent = static_cast<BrowserParent*>(Manager()); + browserParent->VisitAll([&flushPromises](BrowserParent* aBrowser) { + if (PSessionStoreParent* sessionStoreParent = + SingleManagedOrNull(aBrowser->ManagedPSessionStoreParent())) { + flushPromises.AppendElement( + static_cast<SessionStoreParent*>(sessionStoreParent) + ->FlushSessionStore()); + } + }); + } + + RefPtr<SessionStoreParent::FlushTabStatePromise::AllPromiseType> + flushPromise = SessionStoreParent::FlushTabStatePromise::All( + GetMainThreadSerialEventTarget(), flushPromises); + + mBrowsingContext->UpdateSessionStoreSessionStorage([aDone, flushPromise]() { + flushPromise->Then(GetCurrentSerialEventTarget(), __func__, + [aDone]() { aDone(); }); + }); +} + +already_AddRefed<SessionStoreParent::FlushTabStatePromise> +SessionStoreParent::FlushSessionStore() { + if (!mBrowsingContext) { + return nullptr; + } + + RefPtr<SessionStoreParent::FlushTabStatePromise> promise = + SendFlushTabState(); + return promise.forget(); +} + +void SessionStoreParent::FinalFlushAllSessionStoreChildren( + const std::function<void()>& aDone) { + if (!mBrowsingContext) { + aDone(); + return; + } + + SessionStoreChild* sessionStoreChild = + static_cast<SessionStoreChild*>(InProcessParent::ChildActorFor(this)); + if (!sessionStoreChild || mozilla::SessionHistoryInParent()) { + return FlushAllSessionStoreChildren(aDone); + } + + sessionStoreChild->FlushSessionStore(); + mBrowsingContext->UpdateSessionStoreSessionStorage(aDone); +} + +mozilla::ipc::IPCResult SessionStoreParent::RecvSessionStoreUpdate( + const Maybe<nsCString>& aDocShellCaps, const Maybe<bool>& aPrivatedMode, + const MaybeSessionStoreZoom& aZoom, const bool aNeedCollectSHistory, + const uint32_t& aEpoch) { + if (!mBrowsingContext) { + return IPC_OK(); + } + + RefPtr<SessionStoreFormData> formData = + mHasNewFormData ? mSessionStore->GetFormdata() : nullptr; + RefPtr<SessionStoreScrollData> scroll = + mHasNewScrollPosition ? mSessionStore->GetScroll() : nullptr; + + DoSessionStoreUpdate(mBrowsingContext, aDocShellCaps, aPrivatedMode, formData, + scroll, aZoom, aNeedCollectSHistory, aEpoch); + + mHasNewFormData = false; + mHasNewScrollPosition = false; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult SessionStoreParent::RecvIncrementalSessionStoreUpdate( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, + const Maybe<FormData>& aFormData, const Maybe<nsPoint>& aScrollPosition, + uint32_t aEpoch) { + if (!aBrowsingContext.IsNull()) { + if (aFormData.isSome()) { + mHasNewFormData = true; + } + if (aScrollPosition.isSome()) { + mHasNewScrollPosition = true; + } + + mSessionStore->UpdateSessionStore( + aBrowsingContext.GetMaybeDiscarded()->Canonical(), aFormData, + aScrollPosition, aEpoch); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult SessionStoreParent::RecvResetSessionStore( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, uint32_t aEpoch) { + if (!aBrowsingContext.IsNull()) { + mSessionStore->RemoveSessionStore( + aBrowsingContext.GetMaybeDiscarded()->Canonical()); + } + return IPC_OK(); +} + +void SessionStoreParent::SessionStoreUpdate( + const Maybe<nsCString>& aDocShellCaps, const Maybe<bool>& aPrivatedMode, + const MaybeSessionStoreZoom& aZoom, const bool aNeedCollectSHistory, + const uint32_t& aEpoch) { + Unused << RecvSessionStoreUpdate(aDocShellCaps, aPrivatedMode, aZoom, + aNeedCollectSHistory, aEpoch); +} + +void SessionStoreParent::IncrementalSessionStoreUpdate( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, + const Maybe<FormData>& aFormData, const Maybe<nsPoint>& aScrollPosition, + uint32_t aEpoch) { + Unused << RecvIncrementalSessionStoreUpdate(aBrowsingContext, aFormData, + aScrollPosition, aEpoch); +} + +void SessionStoreParent::ResetSessionStore( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, uint32_t aEpoch) { + Unused << RecvResetSessionStore(aBrowsingContext, aEpoch); +} + +NS_IMPL_CYCLE_COLLECTION(SessionStoreParent, mBrowsingContext, mSessionStore) diff --git a/toolkit/components/sessionstore/SessionStoreParent.h b/toolkit/components/sessionstore/SessionStoreParent.h new file mode 100644 index 0000000000..0cf723985e --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreParent.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreParent_h +#define mozilla_dom_SessionStoreParent_h + +#include "mozilla/dom/BrowserSessionStore.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/PSessionStoreParent.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/SessionStoreScrollData.h" + +namespace mozilla::dom { +class BrowserParent; + +class SessionStoreParent final : public PSessionStoreParent { + public: + SessionStoreParent(CanonicalBrowsingContext* aBrowsingContext, + BrowserSessionStore* aSessionStore); + + void FlushAllSessionStoreChildren(const std::function<void()>& aDone); + + void FinalFlushAllSessionStoreChildren(const std::function<void()>& aDone); + + /** + * Sends data to be stored and instructions to the session store to + * potentially collect data in the parent. + */ + mozilla::ipc::IPCResult RecvSessionStoreUpdate( + const Maybe<nsCString>& aDocShellCaps, const Maybe<bool>& aPrivatedMode, + const MaybeSessionStoreZoom& aZoom, const bool aNeedCollectSHistory, + const uint32_t& aEpoch); + + mozilla::ipc::IPCResult RecvIncrementalSessionStoreUpdate( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, + const Maybe<FormData>& aFormData, const Maybe<nsPoint>& aScrollPosition, + uint32_t aEpoch); + + mozilla::ipc::IPCResult RecvResetSessionStore( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, uint32_t aEpoch); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(SessionStoreParent) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(SessionStoreParent) + + protected: + friend class SessionStoreChild; + void SessionStoreUpdate(const Maybe<nsCString>& aDocShellCaps, + const Maybe<bool>& aPrivatedMode, + const MaybeSessionStoreZoom& aZoom, + const bool aNeedCollectSHistory, + const uint32_t& aEpoch); + + void IncrementalSessionStoreUpdate( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, + const Maybe<FormData>& aFormData, const Maybe<nsPoint>& aScrollPosition, + uint32_t aEpoch); + + void ResetSessionStore( + const MaybeDiscarded<BrowsingContext>& aBrowsingContext, uint32_t aEpoch); + + private: + ~SessionStoreParent() = default; + + already_AddRefed<SessionStoreParent::FlushTabStatePromise> + FlushSessionStore(); + + bool mHasNewFormData = false; + bool mHasNewScrollPosition = false; + + RefPtr<CanonicalBrowsingContext> mBrowsingContext; + RefPtr<BrowserSessionStore> mSessionStore; +}; +} // namespace mozilla::dom + +#endif // mozilla_dom_SessionStoreParent_h diff --git a/toolkit/components/sessionstore/SessionStoreRestoreData.cpp b/toolkit/components/sessionstore/SessionStoreRestoreData.cpp new file mode 100644 index 0000000000..fbb96e1b87 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreRestoreData.cpp @@ -0,0 +1,210 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CustomElementTypes.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/SessionStoreUtils.h" +#include "mozilla/dom/sessionstore/SessionStoreTypes.h" +#include "mozilla/dom/WindowContext.h" +#include "nsISessionStoreRestoreData.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace dom { + +bool SessionStoreRestoreData::IsEmpty() { + return (!mURI && mScroll.IsEmpty() && mInnerHTML.IsEmpty() && + mEntries.IsEmpty() && mChildren.IsEmpty()); +} + +SessionStoreRestoreData* SessionStoreRestoreData::FindDataForChild( + BrowsingContext* aContext) { + nsTArray<uint32_t> path; + if (!aContext->GetOffsetPath(path)) { + return nullptr; + } + + SessionStoreRestoreData* data = this; + + for (uint32_t offset : Reversed(path)) { + if (!data || data->mChildren.Length() <= offset || + !data->mChildren[offset] || data->mChildren[offset]->IsEmpty()) { + return nullptr; + } + data = data->mChildren[offset]; + } + + return data; +} + +bool SessionStoreRestoreData::CanRestoreInto(nsIURI* aDocumentURI) { + if (!mURI) { + // This should mean that we don't have form data. It's fine to restore this + // data into any document — the worst that will happen is that we restore an + // incorrect scroll position. + MOZ_ASSERT(mEntries.IsEmpty()); + MOZ_ASSERT(mInnerHTML.IsEmpty()); + return true; + } + bool equalsExceptRef = false; + return (aDocumentURI && + NS_SUCCEEDED(mURI->EqualsExceptRef(aDocumentURI, &equalsExceptRef)) && + equalsExceptRef); +} + +bool SessionStoreRestoreData::RestoreInto(RefPtr<BrowsingContext> aContext) { + if (!aContext->IsInProcess()) { + return false; + } + + if (WindowContext* window = aContext->GetCurrentWindowContext()) { + if (!mScroll.IsEmpty()) { + if (nsGlobalWindowInner* inner = window->GetInnerWindow()) { + SessionStoreUtils::RestoreScrollPosition(*inner, mScroll); + } + } + if (mURI) { + if (nsCOMPtr<Document> doc = window->GetExtantDoc()) { + if (!CanRestoreInto(doc->GetDocumentURI())) { + return false; + } + SessionStoreUtils::RestoreFormData(*doc, mInnerHTML, mEntries); + } + } + } + + return true; +} + +void SessionStoreRestoreData::AddFormEntry( + bool aIsXPath, const nsAString& aIdOrXPath, + sessionstore::FormEntryValue aValue) { + mEntries.AppendElement( + Entry{sessionstore::FormEntry{nsString(aIdOrXPath), aValue}, aIsXPath}); +} + +NS_IMPL_ISUPPORTS(SessionStoreRestoreData, nsISessionStoreRestoreData, + SessionStoreRestoreData) + +NS_IMETHODIMP +SessionStoreRestoreData::GetUrl(nsACString& aURL) { + if (mURI) { + nsresult rv = mURI->GetSpec(aURL); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::SetUrl(const nsACString& aURL) { + nsresult rv = NS_NewURI(getter_AddRefs(mURI), aURL); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::GetInnerHTML(nsAString& aInnerHTML) { + aInnerHTML = mInnerHTML; + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::SetInnerHTML(const nsAString& aInnerHTML) { + mInnerHTML = aInnerHTML; + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::GetScroll(nsACString& aScroll) { + aScroll = mScroll; + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::SetScroll(const nsACString& aScroll) { + mScroll = aScroll; + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::AddTextField(bool aIsXPath, + const nsAString& aIdOrXPath, + const nsAString& aValue) { + AddFormEntry(aIsXPath, aIdOrXPath, sessionstore::TextField{nsString(aValue)}); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::AddCheckbox(bool aIsXPath, const nsAString& aIdOrXPath, + const bool aValue) { + AddFormEntry(aIsXPath, aIdOrXPath, sessionstore::Checkbox{aValue}); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::AddFileList(bool aIsXPath, const nsAString& aIdOrXPath, + const nsAString& aType, + const nsTArray<nsString>& aFileList) { + AddFormEntry(aIsXPath, aIdOrXPath, sessionstore::FileList{aFileList}); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::AddSingleSelect(bool aIsXPath, + const nsAString& aIdOrXPath, + uint32_t aSelectedIndex, + const nsAString& aValue) { + AddFormEntry(aIsXPath, aIdOrXPath, + sessionstore::SingleSelect{aSelectedIndex, nsString(aValue)}); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::AddMultipleSelect(bool aIsXPath, + const nsAString& aIdOrXPath, + const nsTArray<nsString>& aValues) { + AddFormEntry(aIsXPath, aIdOrXPath, sessionstore::MultipleSelect{aValues}); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::AddCustomElement(bool aIsXPath, + const nsAString& aIdOrXPath, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aState) { + AutoJSContext cx; + Nullable<OwningFileOrUSVStringOrFormData> value; + if (!aValue.isNull() && !value.SetValue().Init(cx, aValue)) { + return NS_ERROR_FAILURE; + } + Nullable<OwningFileOrUSVStringOrFormData> state; + if (!aState.isNull() && !state.SetValue().Init(cx, aState)) { + return NS_ERROR_FAILURE; + } + AddFormEntry(aIsXPath, aIdOrXPath, + CustomElementTuple( + nsContentUtils::ConvertToCustomElementFormValue(value), + nsContentUtils::ConvertToCustomElementFormValue(state))); + return NS_OK; +} + +NS_IMETHODIMP +SessionStoreRestoreData::AddChild(nsISessionStoreRestoreData* aChild, + uint32_t aIndex) { + if (nsCOMPtr<SessionStoreRestoreData> child = do_QueryInterface(aChild)) { + if (aIndex > mChildren.Length()) { + mChildren.SetLength(aIndex); + } + mChildren.InsertElementAt(aIndex, child); + } + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/toolkit/components/sessionstore/SessionStoreRestoreData.h b/toolkit/components/sessionstore/SessionStoreRestoreData.h new file mode 100644 index 0000000000..4952045e9d --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreRestoreData.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreRestoreData_h +#define mozilla_dom_SessionStoreRestoreData_h + +#include "mozilla/dom/sessionstore/SessionStoreTypes.h" +#include "mozilla/dom/SessionStoreData.h" +#include "nsISessionStoreRestoreData.h" + +namespace mozilla::dom { + +#define NS_SESSIONSTORERESTOREDATA_IID \ + { \ + 0x88800c5b, 0xe142, 0x4ac6, { \ + 0x91, 0x52, 0xca, 0xbd, 0x3b, 0x74, 0xb9, 0xf8 \ + } \ + } + +class SessionStoreRestoreData final : public nsISessionStoreRestoreData { + public: + SessionStoreRestoreData() = default; + bool IsEmpty(); + SessionStoreRestoreData* FindDataForChild(BrowsingContext* aContext); + bool CanRestoreInto(nsIURI* aDocumentURI); + MOZ_CAN_RUN_SCRIPT bool RestoreInto(RefPtr<BrowsingContext> aContext); + + NS_DECL_ISUPPORTS + NS_DECL_NSISESSIONSTORERESTOREDATA + NS_DECLARE_STATIC_IID_ACCESSOR(NS_SESSIONSTORERESTOREDATA_IID) + + struct Entry { + sessionstore::FormEntry mData; + bool mIsXPath; + }; + + private: + virtual ~SessionStoreRestoreData() = default; + void AddFormEntry(bool aIsXPath, const nsAString& aIdOrXPath, + sessionstore::FormEntryValue aValue); + + nsCString mScroll; + nsCOMPtr<nsIURI> mURI; + nsString mInnerHTML; + nsTArray<Entry> mEntries; + nsTArray<RefPtr<SessionStoreRestoreData>> mChildren; + + friend struct mozilla::ipc::IPDLParamTraits<SessionStoreRestoreData*>; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(SessionStoreRestoreData, + NS_SESSIONSTORERESTOREDATA_IID) + +} // namespace mozilla::dom + +#endif diff --git a/toolkit/components/sessionstore/SessionStoreScrollData.cpp b/toolkit/components/sessionstore/SessionStoreScrollData.cpp new file mode 100644 index 0000000000..29d58befaa --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreScrollData.cpp @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/SessionStoreScrollData.h" +#include <utility> + +#include "js/PropertyAndElement.h" +#include "js/TypeDecls.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/BrowserSessionStoreBinding.h" +#include "mozilla/dom/BrowsingContext.h" + +#include "nsPresContext.h" + +#include "mozilla/dom/WebIDLGlobalNameHash.h" +#include "js/PropertyDescriptor.h" + +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/GeneratedAtomList.h" +#include "js/StructuredClone.h" +#include "mozilla/dom/DOMJSClass.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "nsContentUtils.h" +#include "js/Array.h" +#include "js/JSON.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(SessionStoreScrollData) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SessionStoreScrollData) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK_PTR(SessionStoreScrollData, + mChildren) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SessionStoreScrollData) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +nsISupports* SessionStoreScrollData::GetParentObject() const { + return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); + ; +} + +JSObject* SessionStoreScrollData::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return SessionStoreScrollData_Binding::Wrap(aCx, this, aGivenProto); +} + +void SessionStoreScrollData::GetScroll(nsACString& aScroll) const { + int scrollX = nsPresContext::AppUnitsToIntCSSPixels(mScroll.x); + int scrollY = nsPresContext::AppUnitsToIntCSSPixels(mScroll.y); + + if ((scrollX != 0) || (scrollY != 0)) { + aScroll = nsPrintfCString("%d,%d", scrollX, scrollY); + } else { + aScroll.SetIsVoid(true); + } +} + +SessionStoreScrollData::ChildrenArray& SessionStoreScrollData::Children() { + return mChildren; +} + +void SessionStoreScrollData::GetChildren( + Nullable<ChildrenArray>& aChildren) const { + if (!mChildren.IsEmpty()) { + aChildren.SetValue() = mChildren.Clone(); + } else { + aChildren.SetNull(); + } +} + +void SessionStoreScrollData::ToJSON(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval) { + JS::Rooted<JSObject*> self(aCx); + { + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, this, &value)) { + return; + } + + self.set(value.toObjectOrNull()); + } + + JS::Rooted<JSObject*> result(aCx, JS_NewPlainObject(aCx)); + + if (!IsEmpty()) { + if (HasData(mScroll)) { + if (!SessionStoreUtils::CopyProperty(aCx, result, self, u"scroll"_ns)) { + return; + } + } + + SessionStoreUtils::CopyChildren(aCx, result, mChildren); + } + + aRetval.set(result); +} + +void SessionStoreScrollData::Update(const CollectedType& aUpdate) { + SessionStoreScrollData_Binding::ClearCachedScrollValue(this); + mScroll = aUpdate; +} + +void SessionStoreScrollData::ClearCachedChildren() { + SessionStoreScrollData_Binding::ClearCachedChildrenValue(this); +} + +/* static */ +bool SessionStoreScrollData::HasData(const CollectedType& aPoint) { + int scrollX = nsPresContext::AppUnitsToIntCSSPixels(aPoint.x); + int scrollY = nsPresContext::AppUnitsToIntCSSPixels(aPoint.y); + return scrollX != 0 || scrollY != 0; +} + +bool SessionStoreScrollData::IsEmpty() const { + return !HasData(mScroll) && mChildren.IsEmpty(); +} diff --git a/toolkit/components/sessionstore/SessionStoreScrollData.h b/toolkit/components/sessionstore/SessionStoreScrollData.h new file mode 100644 index 0000000000..b2f5d4bd32 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreScrollData.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreScrollData_h +#define mozilla_dom_SessionStoreScrollData_h + +#include "js/TypeDecls.h" +#include "mozilla/WeakPtr.h" +#include "nsPoint.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/SessionStoreChangeListener.h" + +namespace mozilla::dom { + +class BrowsingContext; +class WindowGlobalParent; +class OwningByteStringOrObjectOrNull; +struct SessionStoreZoomData; + +using SessionStoreZoom = std::tuple<float, uint32_t, uint32_t>; +using MaybeSessionStoreZoom = + mozilla::Maybe<std::tuple<float, uint32_t, uint32_t>>; + +class SessionStoreScrollData final : public nsISupports, + public nsWrapperCache, + public SupportsWeakPtr { + public: + using CollectedType = nsPoint; + using LocationType = WeakPtr<SessionStoreScrollData>; + using ChildrenArray = nsTArray<RefPtr<SessionStoreScrollData>>; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SessionStoreScrollData) + nsISupports* GetParentObject() const; + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetScroll(nsACString& aScroll) const; + + ChildrenArray& Children(); + + void GetChildren(Nullable<ChildrenArray>& aChildren) const; + + void ToJSON(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval); + + void Update(const CollectedType& aUpdate); + + void ClearCachedChildren(); + + static bool HasData(const CollectedType& aPoint); + + bool IsEmpty() const; + + private: + ~SessionStoreScrollData() = default; + + nsPoint mScroll; + MaybeSessionStoreZoom mZoom; + nsTArray<RefPtr<SessionStoreScrollData>> mChildren; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SessionStoreScrollData_h diff --git a/toolkit/components/sessionstore/SessionStoreTypes.ipdlh b/toolkit/components/sessionstore/SessionStoreTypes.ipdlh new file mode 100644 index 0000000000..331005b11a --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreTypes.ipdlh @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +using struct CollectedInputDataValue from "mozilla/dom/SessionStoreMessageUtils.h"; +using struct nsPoint from "nsPoint.h"; + +include CustomElementTypes; +include DOMTypes; + +namespace mozilla { +namespace dom { + +namespace sessionstore { +struct Checkbox { + bool value; +}; + +struct TextField { + nsString value; +}; + +struct FileList { + nsString[] valueList; +}; + +struct SingleSelect { + uint32_t index; + nsString value; +}; + +struct MultipleSelect { + nsString[] valueList; +}; + +union FormEntryValue { + Checkbox; + TextField; + FileList; + SingleSelect; + MultipleSelect; + CustomElementTuple; +}; + +struct FormEntry { + nsString id; + FormEntryValue value; +}; + +struct FormData { + bool hasData; + FormEntry[] id; + FormEntry[] xpath; + nsString innerHTML; + nsCString uri; +}; + +struct DocShellRestoreState { + nullable nsIURI URI; + nsCString docShellCaps; +}; + +} // sessionstore + +} // dom +} // mozilla diff --git a/toolkit/components/sessionstore/SessionStoreUtils.cpp b/toolkit/components/sessionstore/SessionStoreUtils.cpp new file mode 100644 index 0000000000..3c94a54261 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp @@ -0,0 +1,1948 @@ +/* -*- 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 "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject +#include "js/JSON.h" +#include "js/Object.h" +#include "js/PropertyAndElement.h" // JS_GetElement +#include "js/TypeDecls.h" +#include "jsapi.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/AutocompleteInfoBinding.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/CustomElementTypes.h" +#include "mozilla/dom/CustomElementRegistry.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/FragmentOrElement.h" +#include "mozilla/dom/HTMLElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/SessionStorageManager.h" +#include "mozilla/dom/PBackgroundSessionStorageCache.h" +#include "mozilla/dom/SessionStoreChangeListener.h" +#include "mozilla/dom/SessionStoreChild.h" +#include "mozilla/dom/SessionStoreUtils.h" +#include "mozilla/dom/SessionStoreUtilsBinding.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/sessionstore/SessionStoreTypes.h" +#include "mozilla/dom/txIXPathContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "mozilla/dom/XPathResult.h" +#include "mozilla/dom/XPathEvaluator.h" +#include "mozilla/dom/XPathExpression.h" +#include "mozilla/dom/PBackgroundSessionStorageCache.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ReverseIterator.h" +#include "mozilla/UniquePtr.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsContentList.h" +#include "nsContentUtils.h" +#include "nsFocusManager.h" +#include "nsGlobalWindowInner.h" +#include "nsIContentInlines.h" +#include "nsIDocShell.h" +#include "nsIFormControl.h" +#include "nsIScrollableFrame.h" +#include "nsISHistory.h" +#include "nsIXULRuntime.h" +#include "nsPresContext.h" +#include "nsPrintfCString.h" +#include "nsDocShell.h" +#include "nsNetUtil.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::dom::sessionstore; + +namespace { + +class DynamicFrameEventFilter final : public nsIDOMEventListener { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(DynamicFrameEventFilter) + + explicit DynamicFrameEventFilter(EventListener* aListener) + : mListener(aListener) {} + + NS_IMETHODIMP HandleEvent(Event* aEvent) override { + if (mListener && TargetInNonDynamicDocShell(aEvent)) { + mListener->HandleEvent(*aEvent); + } + + return NS_OK; + } + + private: + ~DynamicFrameEventFilter() = default; + + bool TargetInNonDynamicDocShell(Event* aEvent) { + EventTarget* target = aEvent->GetTarget(); + if (!target) { + return false; + } + + nsPIDOMWindowOuter* outer = target->GetOwnerGlobalForBindingsInternal(); + if (!outer || !outer->GetDocShell()) { + return false; + } + + RefPtr<BrowsingContext> context = outer->GetBrowsingContext(); + return context && !context->CreatedDynamically(); + } + + RefPtr<EventListener> mListener; +}; + +NS_IMPL_CYCLE_COLLECTION(DynamicFrameEventFilter, mListener) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DynamicFrameEventFilter) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(DynamicFrameEventFilter) +NS_IMPL_CYCLE_COLLECTING_RELEASE(DynamicFrameEventFilter) + +} // anonymous namespace + +/* static */ +void SessionStoreUtils::ForEachNonDynamicChildFrame( + const GlobalObject& aGlobal, WindowProxyHolder& aWindow, + SessionStoreUtilsFrameCallback& aCallback, ErrorResult& aRv) { + if (!aWindow.get()) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + nsCOMPtr<nsIDocShell> docShell = aWindow.get()->GetDocShell(); + if (!docShell) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + int32_t length; + aRv = docShell->GetInProcessChildCount(&length); + if (aRv.Failed()) { + return; + } + + for (int32_t i = 0; i < length; ++i) { + nsCOMPtr<nsIDocShellTreeItem> item; + docShell->GetInProcessChildAt(i, getter_AddRefs(item)); + if (!item) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<BrowsingContext> context = item->GetBrowsingContext(); + if (!context) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (!context->CreatedDynamically()) { + int32_t childOffset = context->ChildOffset(); + aCallback.Call(WindowProxyHolder(context.forget()), childOffset); + } + } +} + +/* static */ +already_AddRefed<nsISupports> +SessionStoreUtils::AddDynamicFrameFilteredListener( + const GlobalObject& aGlobal, EventTarget& aTarget, const nsAString& aType, + JS::Handle<JS::Value> aListener, bool aUseCapture, bool aMozSystemGroup, + ErrorResult& aRv) { + if (NS_WARN_IF(!aListener.isObject())) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return nullptr; + } + + JSContext* cx = aGlobal.Context(); + JS::Rooted<JSObject*> obj(cx, &aListener.toObject()); + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + RefPtr<EventListener> listener = + new EventListener(cx, obj, global, GetIncumbentGlobal()); + + nsCOMPtr<nsIDOMEventListener> filter(new DynamicFrameEventFilter(listener)); + if (aMozSystemGroup) { + aRv = aTarget.AddSystemEventListener(aType, filter, aUseCapture); + } else { + aRv = aTarget.AddEventListener(aType, filter, aUseCapture); + } + if (aRv.Failed()) { + return nullptr; + } + + return filter.forget(); +} + +/* static */ +void SessionStoreUtils::RemoveDynamicFrameFilteredListener( + const GlobalObject& global, EventTarget& aTarget, const nsAString& aType, + nsISupports* aListener, bool aUseCapture, bool aMozSystemGroup, + ErrorResult& aRv) { + nsCOMPtr<nsIDOMEventListener> listener = do_QueryInterface(aListener); + if (!listener) { + aRv.Throw(NS_ERROR_NO_INTERFACE); + return; + } + + if (aMozSystemGroup) { + aTarget.RemoveSystemEventListener(aType, listener, aUseCapture); + } else { + aTarget.RemoveEventListener(aType, listener, aUseCapture); + } +} + +/* static */ +void SessionStoreUtils::CollectDocShellCapabilities(const GlobalObject& aGlobal, + nsIDocShell* aDocShell, + nsCString& aRetVal) { + bool allow; + +#define TRY_ALLOWPROP(y) \ + PR_BEGIN_MACRO \ + aDocShell->GetAllow##y(&allow); \ + if (!allow) { \ + if (!aRetVal.IsEmpty()) { \ + aRetVal.Append(','); \ + } \ + aRetVal.Append(#y); \ + } \ + PR_END_MACRO + + // Bug 1328013 : Don't collect "AllowJavascript" property + // TRY_ALLOWPROP(Javascript); + TRY_ALLOWPROP(MetaRedirects); + TRY_ALLOWPROP(Subframes); + TRY_ALLOWPROP(Images); + TRY_ALLOWPROP(Media); + TRY_ALLOWPROP(DNSPrefetch); + TRY_ALLOWPROP(WindowControl); + TRY_ALLOWPROP(Auth); + TRY_ALLOWPROP(ContentRetargeting); + TRY_ALLOWPROP(ContentRetargetingOnChildren); +#undef TRY_ALLOWPROP +} + +/* static */ +void SessionStoreUtils::RestoreDocShellCapabilities( + nsIDocShell* aDocShell, const nsCString& aDisallowCapabilities) { + aDocShell->SetAllowMetaRedirects(true); + aDocShell->SetAllowSubframes(true); + aDocShell->SetAllowImages(true); + aDocShell->SetAllowMedia(true); + aDocShell->SetAllowDNSPrefetch(true); + aDocShell->SetAllowWindowControl(true); + aDocShell->SetAllowContentRetargeting(true); + aDocShell->SetAllowContentRetargetingOnChildren(true); + + bool allowJavascript = true; + for (const nsACString& token : + nsCCharSeparatedTokenizer(aDisallowCapabilities, ',').ToRange()) { + if (token.EqualsLiteral("Javascript")) { + allowJavascript = false; + } else if (token.EqualsLiteral("MetaRedirects")) { + aDocShell->SetAllowMetaRedirects(false); + } else if (token.EqualsLiteral("Subframes")) { + aDocShell->SetAllowSubframes(false); + } else if (token.EqualsLiteral("Images")) { + aDocShell->SetAllowImages(false); + } else if (token.EqualsLiteral("Media")) { + aDocShell->SetAllowMedia(false); + } else if (token.EqualsLiteral("DNSPrefetch")) { + aDocShell->SetAllowDNSPrefetch(false); + } else if (token.EqualsLiteral("WindowControl")) { + aDocShell->SetAllowWindowControl(false); + } else if (token.EqualsLiteral("ContentRetargeting")) { + bool allow; + aDocShell->GetAllowContentRetargetingOnChildren(&allow); + aDocShell->SetAllowContentRetargeting( + false); // will also set AllowContentRetargetingOnChildren + aDocShell->SetAllowContentRetargetingOnChildren( + allow); // restore the allowProp to original + } else if (token.EqualsLiteral("ContentRetargetingOnChildren")) { + aDocShell->SetAllowContentRetargetingOnChildren(false); + } + } + + if (!mozilla::SessionHistoryInParent()) { + // With SessionHistoryInParent, this is set from the parent process. + BrowsingContext* bc = aDocShell->GetBrowsingContext(); + Unused << bc->SetAllowJavascript(allowJavascript); + } +} + +static void CollectCurrentScrollPosition(JSContext* aCx, Document& aDocument, + Nullable<CollectedData>& aRetVal) { + PresShell* presShell = aDocument.GetPresShell(); + if (!presShell) { + return; + } + nsPoint scrollPos = presShell->GetVisualViewportOffset(); + int scrollX = nsPresContext::AppUnitsToIntCSSPixels(scrollPos.x); + int scrollY = nsPresContext::AppUnitsToIntCSSPixels(scrollPos.y); + + if ((scrollX != 0) || (scrollY != 0)) { + aRetVal.SetValue().mScroll.Construct() = + nsPrintfCString("%d,%d", scrollX, scrollY); + } +} + +/* static */ +void SessionStoreUtils::RestoreScrollPosition(const GlobalObject& aGlobal, + nsGlobalWindowInner& aWindow, + const CollectedData& aData) { + if (aData.mScroll.WasPassed()) { + RestoreScrollPosition(aWindow, aData.mScroll.Value()); + } +} + +/* static */ +void SessionStoreUtils::RestoreScrollPosition( + nsGlobalWindowInner& aWindow, const nsCString& aScrollPosition) { + using Change = mozilla::dom::SessionStoreChangeListener::Change; + SessionStoreChangeListener::CollectSessionStoreData( + aWindow.GetWindowContext(), EnumSet<Change>(Change::Scroll)); + + nsCCharSeparatedTokenizer tokenizer(aScrollPosition, ','); + nsAutoCString token(tokenizer.nextToken()); + int pos_X = atoi(token.get()); + token = tokenizer.nextToken(); + int pos_Y = atoi(token.get()); + + aWindow.ScrollTo(pos_X, pos_Y); + + if (nsCOMPtr<Document> doc = aWindow.GetExtantDoc()) { + if (nsPresContext* presContext = doc->GetPresContext()) { + if (presContext->IsRootContentDocumentCrossProcess()) { + // Use eMainThread so this takes precedence over session history + // (ScrollFrameHelper::ScrollToRestoredPosition()). + presContext->PresShell()->ScrollToVisual( + CSSPoint::ToAppUnits(CSSPoint(pos_X, pos_Y)), + layers::FrameMetrics::eMainThread, ScrollMode::Instant); + } + } + } +} + +// Implements the Luhn checksum algorithm as described at +// http://wikipedia.org/wiki/Luhn_algorithm +// Number digit lengths vary with network, but should fall within 12-19 range. +// [2] More details at https://en.wikipedia.org/wiki/Payment_card_number +static bool IsValidCCNumber(nsAString& aValue) { + uint32_t total = 0; + uint32_t numLength = 0; + uint32_t strLen = aValue.Length(); + for (uint32_t i = 0; i < strLen; ++i) { + uint32_t idx = strLen - i - 1; + // ignore whitespace and dashes) + char16_t chr = aValue[idx]; + if (IsSpaceCharacter(chr) || chr == '-') { + continue; + } + // If our number is too long, note that fact + ++numLength; + if (numLength > 19) { + return false; + } + // Try to parse the character as a base-10 integer. + nsresult rv = NS_OK; + uint32_t val = Substring(aValue, idx, 1).ToInteger(&rv, 10); + if (NS_FAILED(rv)) { + return false; + } + if (i % 2 == 1) { + val *= 2; + if (val > 9) { + val -= 9; + } + } + total += val; + } + + return numLength >= 12 && total % 10 == 0; +} + +// Limit the number of XPath expressions for performance reasons. See bug +// 477564. +static const uint16_t kMaxTraversedXPaths = 100; + +// A helper function to append a element into mId or mXpath of CollectedData +static Record<nsString, OwningStringOrBooleanOrObject>::EntryType* +AppendEntryToCollectedData(nsINode* aNode, const nsAString& aId, + uint16_t& aGeneratedCount, + Nullable<CollectedData>& aRetVal) { + Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry; + if (!aId.IsEmpty()) { + if (!aRetVal.SetValue().mId.WasPassed()) { + aRetVal.SetValue().mId.Construct(); + } + auto& recordEntries = aRetVal.SetValue().mId.Value().Entries(); + entry = recordEntries.AppendElement(); + entry->mKey = aId; + } else { + if (!aRetVal.SetValue().mXpath.WasPassed()) { + aRetVal.SetValue().mXpath.Construct(); + } + auto& recordEntries = aRetVal.SetValue().mXpath.Value().Entries(); + entry = recordEntries.AppendElement(); + nsAutoString xpath; + aNode->GenerateXPath(xpath); + aGeneratedCount++; + entry->mKey = xpath; + } + return entry; +} + +/* for bool value */ +static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId, + const bool& aValue, + uint16_t& aGeneratedCount, + JSContext* aCx, + Nullable<CollectedData>& aRetVal) { + Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry = + AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); + entry->mValue.SetAsBoolean() = aValue; +} + +/* for nsString value */ +static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId, + const nsString& aValue, + uint16_t& aGeneratedCount, + Nullable<CollectedData>& aRetVal) { + Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry = + AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); + entry->mValue.SetAsString() = aValue; +} + +/* for single select value */ +static void AppendValueToCollectedData( + nsINode* aNode, const nsAString& aId, + const CollectedNonMultipleSelectValue& aValue, uint16_t& aGeneratedCount, + JSContext* aCx, Nullable<CollectedData>& aRetVal) { + JS::Rooted<JS::Value> jsval(aCx); + if (!ToJSValue(aCx, aValue, &jsval)) { + JS_ClearPendingException(aCx); + return; + } + Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry = + AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); + entry->mValue.SetAsObject() = &jsval.toObject(); +} + +/* special handing for input element with string type */ +static void AppendValueToCollectedData(Document& aDocument, nsINode* aNode, + const nsAString& aId, + const nsString& aValue, + uint16_t& aGeneratedCount, + JSContext* aCx, + Nullable<CollectedData>& aRetVal) { + if (!aId.IsEmpty()) { + // We want to avoid saving data for about:sessionrestore as a string. + // Since it's stored in the form as stringified JSON, stringifying + // further causes an explosion of escape characters. cf. bug 467409 + if (aId.EqualsLiteral("sessionData")) { + nsAutoCString url; + Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url); + if (url.EqualsLiteral("about:sessionrestore") || + url.EqualsLiteral("about:welcomeback")) { + JS::Rooted<JS::Value> jsval(aCx); + if (JS_ParseJSON(aCx, aValue.get(), aValue.Length(), &jsval) && + jsval.isObject()) { + Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry = + AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); + entry->mValue.SetAsObject() = &jsval.toObject(); + } else { + JS_ClearPendingException(aCx); + } + return; + } + } + } + AppendValueToCollectedData(aNode, aId, aValue, aGeneratedCount, aRetVal); +} + +/* for nsTArray<nsString>: file and multipleSelect */ +static void AppendValueToCollectedData(nsINode* aNode, const nsAString& aId, + const nsAString& aValueType, + nsTArray<nsString>& aValue, + uint16_t& aGeneratedCount, + JSContext* aCx, + Nullable<CollectedData>& aRetVal) { + JS::Rooted<JS::Value> jsval(aCx); + if (aValueType.EqualsLiteral("file")) { + CollectedFileListValue val; + val.mType = aValueType; + val.mFileList = std::move(aValue); + if (!ToJSValue(aCx, val, &jsval)) { + JS_ClearPendingException(aCx); + return; + } + } else { + if (!ToJSValue(aCx, aValue, &jsval)) { + JS_ClearPendingException(aCx); + return; + } + } + Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry = + AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); + entry->mValue.SetAsObject() = &jsval.toObject(); +} + +/* For form-associated custom element state */ +static void AppendValueToCollectedData( + nsINode* aNode, const nsAString& aId, + const Nullable<OwningFileOrUSVStringOrFormData>& aValue, + const Nullable<OwningFileOrUSVStringOrFormData>& aState, + uint16_t& aGeneratedCount, JSContext* aCx, + Nullable<CollectedData>& aRetVal) { + Record<nsString, OwningStringOrBooleanOrObject>::EntryType* entry = + AppendEntryToCollectedData(aNode, aId, aGeneratedCount, aRetVal); + + CollectedCustomElementValue val; + val.mValue = aValue; + val.mState = aState; + JS::Rooted<JS::Value> jsval(aCx); + if (!ToJSValue(aCx, val, &jsval)) { + JS_ClearPendingException(aCx); + return; + } + entry->mValue.SetAsObject() = &jsval.toObject(); +} + +// This isn't size as in binary size, just a heuristic to not store too large +// fields in session store. See StaticPrefs::browser_sessionstore_dom_form_limit +static uint32_t SizeOfFormEntry(const FormEntryValue& aValue) { + uint32_t size = 0; + switch (aValue.type()) { + case FormEntryValue::TCheckbox: + size = aValue.get_Checkbox().value() ? 4 : 5; + break; + case FormEntryValue::TTextField: + size = aValue.get_TextField().value().Length(); + break; + case FormEntryValue::TFileList: { + for (const auto& value : aValue.get_FileList().valueList()) { + size += value.Length(); + } + break; + } + case FormEntryValue::TSingleSelect: + size = aValue.get_SingleSelect().value().Length(); + break; + case FormEntryValue::TMultipleSelect: { + for (const auto& value : aValue.get_MultipleSelect().valueList()) { + size += value.Length(); + } + break; + } + case FormEntryValue::TCustomElementTuple: { + auto customElementTupleSize = + [](const CustomElementFormValue& value) -> uint32_t { + switch (value.type()) { + case CustomElementFormValue::TBlobImpl: + return value.get_BlobImpl()->GetAllocationSize(); + case CustomElementFormValue::TnsString: + return value.get_nsString().Length(); + case CustomElementFormValue::TArrayOfFormDataTuple: { + uint32_t formDataSize = 0; + for (const auto& entry : value.get_ArrayOfFormDataTuple()) { + formDataSize += entry.name().Length(); + const auto& entryValue = entry.value(); + switch (entryValue.type()) { + case FormDataValue::TBlobImpl: + formDataSize += + entryValue.get_BlobImpl()->GetAllocationSize(); + break; + case FormDataValue::TnsString: + formDataSize += entryValue.get_nsString().Length(); + break; + default: + break; + } + } + return formDataSize; + } + default: + return 0; + } + }; + + auto ceTuple = aValue.get_CustomElementTuple(); + size += customElementTupleSize(ceTuple.value()); + size += customElementTupleSize(ceTuple.state()); + break; + } + default: + break; + } + return size; +} + +static uint32_t AppendEntry(nsINode* aNode, const nsString& aId, + const FormEntryValue& aValue, + sessionstore::FormData& aFormData) { + uint32_t size = SizeOfFormEntry(aValue); + if (size > StaticPrefs::browser_sessionstore_dom_form_limit()) { + return 0; + } + + if (aId.IsEmpty()) { + FormEntry* entry = aFormData.xpath().AppendElement(); + entry->value() = aValue; + aNode->GenerateXPath(entry->id()); + size += entry->id().Length(); + } else { + aFormData.id().AppendElement(FormEntry{aId, aValue}); + size += aId.Length(); + } + + return size; +} + +static uint32_t CollectTextAreaElement(Document* aDocument, + sessionstore::FormData& aFormData) { + uint32_t size = 0; + RefPtr<nsContentList> textlist = + NS_GetContentList(aDocument, kNameSpaceID_XHTML, u"textarea"_ns); + uint32_t length = textlist->Length(); + for (uint32_t i = 0; i < length; ++i) { + MOZ_ASSERT(textlist->Item(i), "null item in node list!"); + + auto* textArea = HTMLTextAreaElement::FromNodeOrNull(textlist->Item(i)); + if (!textArea) { + continue; + } + DOMString autocomplete; + textArea->GetAutocomplete(autocomplete); + if (autocomplete.AsAString().EqualsLiteral("off")) { + continue; + } + nsAutoString id; + textArea->GetId(id); + if (id.IsEmpty() && (aFormData.xpath().Length() > kMaxTraversedXPaths)) { + continue; + } + nsString value; + textArea->GetValue(value); + // In order to reduce XPath generation (which is slow), we only save data + // for form fields that have been changed. (cf. bug 537289) + if (textArea->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value, + eCaseMatters)) { + continue; + } + + size += AppendEntry(textArea, id, TextField{value}, aFormData); + } + + return size; +} + +static uint32_t CollectInputElement(Document* aDocument, + sessionstore::FormData& aFormData) { + uint32_t size = 0; + RefPtr<nsContentList> inputlist = + NS_GetContentList(aDocument, kNameSpaceID_XHTML, u"input"_ns); + uint32_t length = inputlist->Length(); + for (uint32_t i = 0; i < length; ++i) { + MOZ_ASSERT(inputlist->Item(i), "null item in node list!"); + nsCOMPtr<nsIFormControl> formControl = + do_QueryInterface(inputlist->Item(i)); + if (formControl) { + auto controlType = formControl->ControlType(); + if (controlType == FormControlType::InputPassword || + controlType == FormControlType::InputHidden || + controlType == FormControlType::InputButton || + controlType == FormControlType::InputImage || + controlType == FormControlType::InputSubmit || + controlType == FormControlType::InputReset) { + continue; + } + } + RefPtr<HTMLInputElement> input = + HTMLInputElement::FromNodeOrNull(inputlist->Item(i)); + if (!input || !nsContentUtils::IsAutocompleteEnabled(input)) { + continue; + } + nsAutoString id; + input->GetId(id); + if (id.IsEmpty() && (aFormData.xpath().Length() > kMaxTraversedXPaths)) { + continue; + } + Nullable<AutocompleteInfo> aInfo; + input->GetAutocompleteInfo(aInfo); + if (!aInfo.IsNull() && !aInfo.Value().mCanAutomaticallyPersist) { + continue; + } + + FormEntryValue value; + if (input->ControlType() == FormControlType::InputCheckbox || + input->ControlType() == FormControlType::InputRadio) { + bool checked = input->Checked(); + if (checked == input->DefaultChecked()) { + continue; + } + size += AppendEntry(input, id, Checkbox{checked}, aFormData); + } else if (input->ControlType() == FormControlType::InputFile) { + IgnoredErrorResult rv; + sessionstore::FileList file; + input->MozGetFileNameArray(file.valueList(), rv); + if (rv.Failed() || file.valueList().IsEmpty()) { + continue; + } + size += AppendEntry(input, id, file, aFormData); + } else { + TextField field; + input->GetValue(field.value(), CallerType::System); + auto& value = field.value(); + // In order to reduce XPath generation (which is slow), we only save data + // for form fields that have been changed. (cf. bug 537289) + // Also, don't want to collect credit card number. + if (value.IsEmpty() || IsValidCCNumber(value) || + input->HasBeenTypePassword() || + input->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value, + eCaseMatters)) { + continue; + } + size += AppendEntry(input, id, field, aFormData); + } + } + + return size; +} + +static uint32_t CollectSelectElement(Document* aDocument, + sessionstore::FormData& aFormData) { + uint32_t size = 0; + RefPtr<nsContentList> selectlist = + NS_GetContentList(aDocument, kNameSpaceID_XHTML, u"select"_ns); + uint32_t length = selectlist->Length(); + for (uint32_t i = 0; i < length; ++i) { + MOZ_ASSERT(selectlist->Item(i), "null item in node list!"); + RefPtr<HTMLSelectElement> select = + HTMLSelectElement::FromNodeOrNull(selectlist->Item(i)); + if (!select) { + continue; + } + nsAutoString id; + select->GetId(id); + if (id.IsEmpty() && (aFormData.xpath().Length() > kMaxTraversedXPaths)) { + continue; + } + AutocompleteInfo aInfo; + select->GetAutocompleteInfo(aInfo); + if (!aInfo.mCanAutomaticallyPersist) { + continue; + } + + if (!select->Multiple()) { + HTMLOptionsCollection* options = select->GetOptions(); + if (!options) { + continue; + } + + uint32_t numOptions = options->Length(); + int32_t defaultIndex = 0; + for (uint32_t idx = 0; idx < numOptions; idx++) { + HTMLOptionElement* option = options->ItemAsOption(idx); + if (option->DefaultSelected()) { + defaultIndex = option->Index(); + } + } + + int32_t selectedIndex = select->SelectedIndex(); + if (selectedIndex == defaultIndex || selectedIndex < 0) { + continue; + } + + DOMString selectVal; + select->GetValue(selectVal); + size += AppendEntry(select, id, + SingleSelect{static_cast<uint32_t>(selectedIndex), + selectVal.AsAString()}, + aFormData); + } else { + HTMLOptionsCollection* options = select->GetOptions(); + if (!options) { + continue; + } + bool hasDefaultValue = true; + nsTArray<nsString> selectslist; + uint32_t numOptions = options->Length(); + for (uint32_t idx = 0; idx < numOptions; idx++) { + HTMLOptionElement* option = options->ItemAsOption(idx); + bool selected = option->Selected(); + + hasDefaultValue = + hasDefaultValue && (selected == option->DefaultSelected()); + + if (!selected) { + continue; + } + option->GetValue(*selectslist.AppendElement()); + } + // In order to reduce XPath generation (which is slow), we only save data + // for form fields that have been changed. (cf. bug 537289) + if (hasDefaultValue) { + continue; + } + + size += AppendEntry(select, id, MultipleSelect{selectslist}, aFormData); + } + } + + return size; +} + +static already_AddRefed<nsContentList> GetFormAssociatedCustomElements( + nsINode* aRootNode) { + MOZ_ASSERT(aRootNode, "Content list has to have a root"); + + auto matchFunc = [](Element* aElement, int32_t aNamespace, nsAtom* aAtom, + void* aData) -> bool { + return aElement->HasCustomElementData() && + aElement->GetCustomElementData()->IsFormAssociated(); + }; + RefPtr<nsContentList> list = + new nsContentList(aRootNode, matchFunc, nullptr, nullptr); + return list.forget(); +} + +static uint32_t CollectFormAssociatedCustomElement( + Document* aDocument, sessionstore::FormData& aFormData) { + uint32_t size = 0; + RefPtr<nsContentList> faceList = GetFormAssociatedCustomElements(aDocument); + uint32_t length = faceList->Length(); + for (uint32_t i = 0; i < length; ++i) { + MOZ_ASSERT(faceList->Item(i), "null item in node list!"); + RefPtr<Element> element = Element::FromNode(faceList->Item(i)); + + nsAutoString id; + element->GetId(id); + if (id.IsEmpty() && (aFormData.xpath().Length() > kMaxTraversedXPaths)) { + continue; + } + + const auto* internals = + element->GetCustomElementData()->GetElementInternals(); + auto formState = internals->GetFormState(); + auto formValue = internals->GetFormSubmissionValue(); + if (formState.IsNull() && formValue.IsNull()) { + continue; + } + + CustomElementTuple entry; + entry.value() = nsContentUtils::ConvertToCustomElementFormValue(formValue); + entry.state() = nsContentUtils::ConvertToCustomElementFormValue(formState); + size += AppendEntry(element, id, entry, aFormData); + } + return size; +} + +/* static */ +uint32_t SessionStoreUtils::CollectFormData(Document* aDocument, + sessionstore::FormData& aFormData) { + MOZ_DIAGNOSTIC_ASSERT(aDocument); + uint32_t size = 0; + size += CollectTextAreaElement(aDocument, aFormData); + size += CollectInputElement(aDocument, aFormData); + size += CollectSelectElement(aDocument, aFormData); + size += CollectFormAssociatedCustomElement(aDocument, aFormData); + + aFormData.hasData() = + !aFormData.id().IsEmpty() || !aFormData.xpath().IsEmpty(); + + return size; +} + +/* static */ +template <typename... ArgsT> +void SessionStoreUtils::CollectFromTextAreaElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args) { + RefPtr<nsContentList> textlist = + NS_GetContentList(&aDocument, kNameSpaceID_XHTML, u"textarea"_ns); + uint32_t length = textlist->Length(true); + for (uint32_t i = 0; i < length; ++i) { + MOZ_ASSERT(textlist->Item(i), "null item in node list!"); + + HTMLTextAreaElement* textArea = + HTMLTextAreaElement::FromNodeOrNull(textlist->Item(i)); + if (!textArea) { + continue; + } + DOMString autocomplete; + textArea->GetAutocomplete(autocomplete); + if (autocomplete.AsAString().EqualsLiteral("off")) { + continue; + } + nsAutoString id; + textArea->GetId(id); + if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) { + continue; + } + nsString value; + textArea->GetValue(value); + // In order to reduce XPath generation (which is slow), we only save data + // for form fields that have been changed. (cf. bug 537289) + if (textArea->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value, + eCaseMatters)) { + continue; + } + AppendValueToCollectedData(textArea, id, value, aGeneratedCount, + std::forward<ArgsT>(args)...); + } +} + +/* static */ +template <typename... ArgsT> +void SessionStoreUtils::CollectFromInputElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args) { + RefPtr<nsContentList> inputlist = + NS_GetContentList(&aDocument, kNameSpaceID_XHTML, u"input"_ns); + uint32_t length = inputlist->Length(true); + for (uint32_t i = 0; i < length; ++i) { + MOZ_ASSERT(inputlist->Item(i), "null item in node list!"); + nsCOMPtr<nsIFormControl> formControl = + do_QueryInterface(inputlist->Item(i)); + if (formControl) { + auto controlType = formControl->ControlType(); + if (controlType == FormControlType::InputPassword || + controlType == FormControlType::InputHidden || + controlType == FormControlType::InputButton || + controlType == FormControlType::InputImage || + controlType == FormControlType::InputSubmit || + controlType == FormControlType::InputReset) { + continue; + } + } + RefPtr<HTMLInputElement> input = + HTMLInputElement::FromNodeOrNull(inputlist->Item(i)); + if (!input || !nsContentUtils::IsAutocompleteEnabled(input)) { + continue; + } + nsAutoString id; + input->GetId(id); + if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) { + continue; + } + Nullable<AutocompleteInfo> aInfo; + input->GetAutocompleteInfo(aInfo); + if (!aInfo.IsNull() && !aInfo.Value().mCanAutomaticallyPersist) { + continue; + } + + if (input->ControlType() == FormControlType::InputCheckbox || + input->ControlType() == FormControlType::InputRadio) { + bool checked = input->Checked(); + if (checked == input->DefaultChecked()) { + continue; + } + AppendValueToCollectedData(input, id, checked, aGeneratedCount, + std::forward<ArgsT>(args)...); + } else if (input->ControlType() == FormControlType::InputFile) { + IgnoredErrorResult rv; + nsTArray<nsString> result; + input->MozGetFileNameArray(result, rv); + if (rv.Failed() || result.Length() == 0) { + continue; + } + AppendValueToCollectedData(input, id, u"file"_ns, result, aGeneratedCount, + std::forward<ArgsT>(args)...); + } else { + nsString value; + input->GetValue(value, CallerType::System); + // In order to reduce XPath generation (which is slow), we only save data + // for form fields that have been changed. (cf. bug 537289) + // Also, don't want to collect credit card number. + if (value.IsEmpty() || IsValidCCNumber(value) || + input->HasBeenTypePassword() || + input->AttrValueIs(kNameSpaceID_None, nsGkAtoms::value, value, + eCaseMatters)) { + continue; + } + AppendValueToCollectedData(aDocument, input, id, value, aGeneratedCount, + std::forward<ArgsT>(args)...); + } + } +} + +/* static */ +template <typename... ArgsT> +void SessionStoreUtils::CollectFromSelectElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args) { + RefPtr<nsContentList> selectlist = + NS_GetContentList(&aDocument, kNameSpaceID_XHTML, u"select"_ns); + uint32_t length = selectlist->Length(true); + for (uint32_t i = 0; i < length; ++i) { + MOZ_ASSERT(selectlist->Item(i), "null item in node list!"); + RefPtr<HTMLSelectElement> select = + HTMLSelectElement::FromNodeOrNull(selectlist->Item(i)); + if (!select) { + continue; + } + nsAutoString id; + select->GetId(id); + if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) { + continue; + } + AutocompleteInfo aInfo; + select->GetAutocompleteInfo(aInfo); + if (!aInfo.mCanAutomaticallyPersist) { + continue; + } + nsAutoCString value; + if (!select->Multiple()) { + // <select>s without the multiple attribute are hard to determine the + // default value, so assume we don't have the default. + DOMString selectVal; + select->GetValue(selectVal); + CollectedNonMultipleSelectValue val; + val.mSelectedIndex = select->SelectedIndex(); + val.mValue = selectVal.AsAString(); + AppendValueToCollectedData(select, id, val, aGeneratedCount, + std::forward<ArgsT>(args)...); + } else { + // <select>s with the multiple attribute are easier to determine the + // default value since each <option> has a defaultSelected property + HTMLOptionsCollection* options = select->GetOptions(); + if (!options) { + continue; + } + bool hasDefaultValue = true; + nsTArray<nsString> selectslist; + uint32_t numOptions = options->Length(); + for (uint32_t idx = 0; idx < numOptions; idx++) { + HTMLOptionElement* option = options->ItemAsOption(idx); + bool selected = option->Selected(); + if (!selected) { + continue; + } + option->GetValue(*selectslist.AppendElement()); + hasDefaultValue = + hasDefaultValue && (selected == option->DefaultSelected()); + } + // In order to reduce XPath generation (which is slow), we only save data + // for form fields that have been changed. (cf. bug 537289) + if (hasDefaultValue) { + continue; + } + + AppendValueToCollectedData(select, id, u"multipleSelect"_ns, selectslist, + aGeneratedCount, std::forward<ArgsT>(args)...); + } + } +} + +/* static */ +template <typename... ArgsT> +void SessionStoreUtils::CollectFromFormAssociatedCustomElement( + Document& aDocument, uint16_t& aGeneratedCount, ArgsT&&... args) { + RefPtr<nsContentList> faceList = GetFormAssociatedCustomElements(&aDocument); + uint32_t length = faceList->Length(true); + for (uint32_t i = 0; i < length; ++i) { + MOZ_ASSERT(faceList->Item(i), "null item in node list!"); + RefPtr<Element> element = Element::FromNode(faceList->Item(i)); + + nsAutoString id; + element->GetId(id); + if (id.IsEmpty() && (aGeneratedCount > kMaxTraversedXPaths)) { + continue; + } + + auto* internals = element->GetCustomElementData()->GetElementInternals(); + const auto& state = internals->GetFormState(); + const auto& value = internals->GetFormSubmissionValue(); + if (state.IsNull() && value.IsNull()) { + continue; + } + + AppendValueToCollectedData(element, id, value, state, aGeneratedCount, + std::forward<ArgsT>(args)...); + } +} + +static void CollectCurrentFormData(JSContext* aCx, Document& aDocument, + Nullable<CollectedData>& aRetVal) { + uint16_t generatedCount = 0; + /* textarea element */ + SessionStoreUtils::CollectFromTextAreaElement(aDocument, generatedCount, + aRetVal); + /* input element */ + SessionStoreUtils::CollectFromInputElement(aDocument, generatedCount, aCx, + aRetVal); + /* select element */ + SessionStoreUtils::CollectFromSelectElement(aDocument, generatedCount, aCx, + aRetVal); + /* form-associated custom element */ + SessionStoreUtils::CollectFromFormAssociatedCustomElement( + aDocument, generatedCount, aCx, aRetVal); + + Element* bodyElement = aDocument.GetBody(); + if (bodyElement && bodyElement->IsInDesignMode()) { + bodyElement->GetInnerHTML(aRetVal.SetValue().mInnerHTML.Construct(), + IgnoreErrors()); + } + + if (aRetVal.IsNull()) { + return; + } + + // Store the frame's current URL with its form data so that we can compare + // it when restoring data to not inject form data into the wrong document. + nsIURI* uri = aDocument.GetDocumentURI(); + if (uri) { + uri->GetSpecIgnoringRef(aRetVal.SetValue().mUrl.Construct()); + } +} + +MOZ_CAN_RUN_SCRIPT +static void SetElementAsString(Element* aElement, const nsAString& aValue) { + IgnoredErrorResult rv; + if (auto* textArea = HTMLTextAreaElement::FromNode(aElement)) { + // Known live because `aElement` is known live. + MOZ_KnownLive(textArea)->SetValue(aValue, rv); + if (!rv.Failed()) { + nsContentUtils::DispatchInputEvent(aElement); + } + return; + } + if (auto* input = HTMLInputElement::FromNode(aElement)) { + input->SetValue(aValue, CallerType::NonSystem, rv); + if (!rv.Failed()) { + nsContentUtils::DispatchInputEvent(aElement); + } + return; + } +} + +MOZ_CAN_RUN_SCRIPT +static void SetElementAsBool(Element* aElement, bool aValue) { + HTMLInputElement* input = HTMLInputElement::FromNode(aElement); + if (input) { + bool checked = input->Checked(); + if (aValue != checked) { + input->SetChecked(aValue); + nsContentUtils::DispatchInputEvent(aElement); + } + } +} + +MOZ_CAN_RUN_SCRIPT +static void SetElementAsFiles(HTMLInputElement* aElement, + const CollectedFileListValue& aValue) { + IgnoredErrorResult rv; + aElement->MozSetFileNameArray(aValue.mFileList, rv); + if (rv.Failed()) { + return; + } + nsContentUtils::DispatchInputEvent(aElement); +} + +MOZ_CAN_RUN_SCRIPT +static void SetElementAsSelect(HTMLSelectElement* aElement, + const CollectedNonMultipleSelectValue& aValue) { + HTMLOptionsCollection* options = aElement->GetOptions(); + if (!options) { + return; + } + int32_t selectIdx = options->SelectedIndex(); + if (selectIdx >= 0) { + nsAutoString selectOptionVal; + options->ItemAsOption(selectIdx)->GetValue(selectOptionVal); + if (aValue.mValue.Equals(selectOptionVal)) { + return; + } + } + uint32_t numOptions = options->Length(); + for (uint32_t idx = 0; idx < numOptions; idx++) { + HTMLOptionElement* option = options->ItemAsOption(idx); + nsAutoString optionValue; + option->GetValue(optionValue); + if (aValue.mValue.Equals(optionValue)) { + aElement->SetSelectedIndex(idx); + nsContentUtils::DispatchInputEvent(aElement); + } + } +} + +MOZ_CAN_RUN_SCRIPT +static void SetElementAsMultiSelect(HTMLSelectElement* aElement, + const nsTArray<nsString>& aValueArray) { + bool fireEvent = false; + HTMLOptionsCollection* options = aElement->GetOptions(); + if (!options) { + return; + } + uint32_t numOptions = options->Length(); + for (uint32_t idx = 0; idx < numOptions; idx++) { + HTMLOptionElement* option = options->ItemAsOption(idx); + nsAutoString optionValue; + option->GetValue(optionValue); + for (uint32_t i = 0, l = aValueArray.Length(); i < l; ++i) { + if (optionValue.Equals(aValueArray[i])) { + option->SetSelected(true); + if (!option->DefaultSelected()) { + fireEvent = true; + } + } + } + } + if (fireEvent) { + nsContentUtils::DispatchInputEvent(aElement); + } +} + +MOZ_CAN_RUN_SCRIPT +static void SetElementAsObject(JSContext* aCx, Element* aElement, + JS::Handle<JS::Value> aObject) { + RefPtr<HTMLInputElement> input = HTMLInputElement::FromNode(aElement); + if (input) { + if (input->ControlType() == FormControlType::InputFile) { + CollectedFileListValue value; + if (value.Init(aCx, aObject)) { + SetElementAsFiles(input, value); + } else { + JS_ClearPendingException(aCx); + } + } + return; + } + RefPtr<HTMLSelectElement> select = HTMLSelectElement::FromNode(aElement); + if (select) { + // For Single Select Element + if (!select->Multiple()) { + CollectedNonMultipleSelectValue value; + if (value.Init(aCx, aObject)) { + SetElementAsSelect(select, value); + } else { + JS_ClearPendingException(aCx); + } + return; + } + + // For Multiple Selects Element + bool isArray = false; + JS::IsArrayObject(aCx, aObject, &isArray); + if (!isArray) { + return; + } + JS::Rooted<JSObject*> arrayObj(aCx, &aObject.toObject()); + uint32_t arrayLength = 0; + if (!JS::GetArrayLength(aCx, arrayObj, &arrayLength)) { + JS_ClearPendingException(aCx); + return; + } + nsTArray<nsString> array(arrayLength); + for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; arrayIdx++) { + JS::Rooted<JS::Value> element(aCx); + if (!JS_GetElement(aCx, arrayObj, arrayIdx, &element)) { + JS_ClearPendingException(aCx); + return; + } + if (!element.isString()) { + return; + } + nsAutoJSString value; + if (!value.init(aCx, element)) { + JS_ClearPendingException(aCx); + return; + } + array.AppendElement(value); + } + SetElementAsMultiSelect(select, array); + } + + // For Form-Associated Custom Element: + if (!aObject.isObject()) { + // Don't restore null values. + return; + } + + auto* data = aElement->GetCustomElementData(); + if (!data || !data->IsFormAssociated()) { + return; + } + auto* internals = data->GetElementInternals(); + + CollectedCustomElementValue value; + if (!value.Init(aCx, aObject)) { + JS_ClearPendingException(aCx); + return; + } + internals->RestoreFormValue(std::move(value.mValue), std::move(value.mState)); +} + +MOZ_CAN_RUN_SCRIPT +static void SetSessionData(JSContext* aCx, Element* aElement, + JS::MutableHandle<JS::Value> aObject) { + nsAutoString data; + if (nsContentUtils::StringifyJSON(aCx, aObject, data, + UndefinedIsNullStringLiteral)) { + SetElementAsString(aElement, data); + } else { + JS_ClearPendingException(aCx); + } +} + +MOZ_CAN_RUN_SCRIPT +static void SetInnerHTML(Document& aDocument, const nsString& aInnerHTML) { + RefPtr<Element> bodyElement = aDocument.GetBody(); + if (bodyElement && bodyElement->IsInDesignMode()) { + IgnoredErrorResult rv; + bodyElement->SetInnerHTML(aInnerHTML, aDocument.NodePrincipal(), rv); + if (!rv.Failed()) { + nsContentUtils::DispatchInputEvent(bodyElement); + } + } +} + +class FormDataParseContext : public txIParseContext { + public: + explicit FormDataParseContext(bool aCaseInsensitive) + : mIsCaseInsensitive(aCaseInsensitive) {} + + nsresult resolveNamespacePrefix(nsAtom* aPrefix, int32_t& aID) override { + if (aPrefix == nsGkAtoms::xul) { + aID = kNameSpaceID_XUL; + } else { + MOZ_ASSERT(nsDependentAtomString(aPrefix).EqualsLiteral("xhtml")); + aID = kNameSpaceID_XHTML; + } + return NS_OK; + } + + nsresult resolveFunctionCall(nsAtom* aName, int32_t aID, + FunctionCall** aFunction) override { + return NS_ERROR_XPATH_UNKNOWN_FUNCTION; + } + + bool caseInsensitiveNameTests() override { return mIsCaseInsensitive; } + + void SetErrorOffset(uint32_t aOffset) override {} + + private: + bool mIsCaseInsensitive; +}; + +static Element* FindNodeByXPath(Document& aDocument, + const nsAString& aExpression) { + FormDataParseContext parsingContext(aDocument.IsHTMLDocument()); + IgnoredErrorResult rv; + UniquePtr<XPathExpression> expression( + aDocument.XPathEvaluator()->CreateExpression(aExpression, &parsingContext, + &aDocument, rv)); + if (rv.Failed()) { + return nullptr; + } + RefPtr<XPathResult> result = expression->Evaluate( + aDocument, XPathResult::FIRST_ORDERED_NODE_TYPE, nullptr, rv); + if (rv.Failed()) { + return nullptr; + } + return Element::FromNodeOrNull(result->GetSingleNodeValue(rv)); +} + +/* static */ +bool SessionStoreUtils::RestoreFormData(const GlobalObject& aGlobal, + Document& aDocument, + const CollectedData& aData) { + if (!aData.mUrl.WasPassed()) { + return true; + } + + // Don't restore any data for the given frame if the URL + // stored in the form data doesn't match its current URL. + nsAutoCString url; + Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url); + if (!aData.mUrl.Value().Equals(url)) { + return false; + } + + using Change = SessionStoreChangeListener::Change; + SessionStoreChangeListener::CollectSessionStoreData( + aDocument.GetWindowContext(), EnumSet<Change>(Change::Input)); + + if (aData.mInnerHTML.WasPassed()) { + SetInnerHTML(aDocument, aData.mInnerHTML.Value()); + } + if (aData.mId.WasPassed()) { + for (auto& entry : aData.mId.Value().Entries()) { + RefPtr<Element> node = aDocument.GetElementById(entry.mKey); + if (node == nullptr) { + continue; + } + if (entry.mValue.IsString()) { + SetElementAsString(node, entry.mValue.GetAsString()); + } else if (entry.mValue.IsBoolean()) { + SetElementAsBool(node, entry.mValue.GetAsBoolean()); + } else { + // For about:{sessionrestore,welcomeback} we saved the field as JSON to + // avoid nested instances causing humongous sessionstore.js files. + // cf. bug 467409 + JSContext* cx = aGlobal.Context(); + if (entry.mKey.EqualsLiteral("sessionData")) { + if (url.EqualsLiteral("about:sessionrestore") || + url.EqualsLiteral("about:welcomeback")) { + JS::Rooted<JS::Value> object( + cx, JS::ObjectValue(*entry.mValue.GetAsObject())); + SetSessionData(cx, node, &object); + continue; + } + } + JS::Rooted<JS::Value> object( + cx, JS::ObjectValue(*entry.mValue.GetAsObject())); + SetElementAsObject(cx, node, object); + } + } + } + + if (aData.mXpath.WasPassed()) { + for (auto& entry : aData.mXpath.Value().Entries()) { + RefPtr<Element> node = FindNodeByXPath(aDocument, entry.mKey); + if (node == nullptr) { + continue; + } + if (entry.mValue.IsString()) { + SetElementAsString(node, entry.mValue.GetAsString()); + } else if (entry.mValue.IsBoolean()) { + SetElementAsBool(node, entry.mValue.GetAsBoolean()); + } else { + JS::Rooted<JS::Value> object( + aGlobal.Context(), JS::ObjectValue(*entry.mValue.GetAsObject())); + SetElementAsObject(aGlobal.Context(), node, object); + } + } + } + + return true; +} + +MOZ_CAN_RUN_SCRIPT +void RestoreFormEntry(Element* aNode, const FormEntryValue& aValue) { + using Type = sessionstore::FormEntryValue::Type; + switch (aValue.type()) { + case Type::TCheckbox: + SetElementAsBool(aNode, aValue.get_Checkbox().value()); + break; + case Type::TTextField: + SetElementAsString(aNode, aValue.get_TextField().value()); + break; + case Type::TFileList: { + if (RefPtr<HTMLInputElement> input = HTMLInputElement::FromNode(aNode); + input && input->ControlType() == FormControlType::InputFile) { + CollectedFileListValue value; + value.mFileList = aValue.get_FileList().valueList().Clone(); + SetElementAsFiles(input, value); + } + break; + } + case Type::TSingleSelect: { + if (RefPtr<HTMLSelectElement> select = HTMLSelectElement::FromNode(aNode); + select && !select->Multiple()) { + CollectedNonMultipleSelectValue value; + value.mSelectedIndex = aValue.get_SingleSelect().index(); + value.mValue = aValue.get_SingleSelect().value(); + SetElementAsSelect(select, value); + } + break; + } + case Type::TMultipleSelect: { + if (RefPtr<HTMLSelectElement> select = HTMLSelectElement::FromNode(aNode); + select && select->Multiple()) { + SetElementAsMultiSelect(select, + aValue.get_MultipleSelect().valueList()); + } + break; + } + case Type::TCustomElementTuple: { + const auto* data = aNode->GetCustomElementData(); + if (!data || !data->IsFormAssociated()) { + return; + } + auto* internals = data->GetElementInternals(); + nsCOMPtr<nsIGlobalObject> global = aNode->GetOwnerGlobal(); + internals->RestoreFormValue( + nsContentUtils::ExtractFormAssociatedCustomElementValue( + global, aValue.get_CustomElementTuple().value()), + nsContentUtils::ExtractFormAssociatedCustomElementValue( + global, aValue.get_CustomElementTuple().state())); + break; + } + default: + MOZ_ASSERT_UNREACHABLE(); + } +} + +/* static */ +void SessionStoreUtils::RestoreFormData( + Document& aDocument, const nsString& aInnerHTML, + const nsTArray<SessionStoreRestoreData::Entry>& aEntries) { + using Change = SessionStoreChangeListener::Change; + SessionStoreChangeListener::CollectSessionStoreData( + aDocument.GetWindowContext(), EnumSet<Change>(Change::Input)); + + if (!aInnerHTML.IsEmpty()) { + SetInnerHTML(aDocument, aInnerHTML); + } + + for (const auto& entry : aEntries) { + RefPtr<Element> node = entry.mIsXPath + ? FindNodeByXPath(aDocument, entry.mData.id()) + : aDocument.GetElementById(entry.mData.id()); + if (node) { + RestoreFormEntry(node, entry.mData.value()); + } + } +} + +typedef void (*CollectorFunc)(JSContext* aCx, Document& aDocument, + Nullable<CollectedData>& aRetVal); + +/** + * A function that will recursively call |CollectorFunc| to collect data for all + * non-dynamic frames in the current frame/docShell tree. + */ +static void CollectFrameTreeData(JSContext* aCx, + BrowsingContext* aBrowsingContext, + Nullable<CollectedData>& aRetVal, + CollectorFunc aFunc) { + if (aBrowsingContext->CreatedDynamically()) { + return; + } + + nsPIDOMWindowOuter* window = aBrowsingContext->GetDOMWindow(); + if (!window || !window->GetDocShell()) { + return; + } + + Document* document = window->GetExtantDoc(); + if (!document) { + return; + } + + /* Collect data from current frame */ + aFunc(aCx, *document, aRetVal); + + /* Collect data from all child frame */ + nsTArray<JSObject*> childrenData; + SequenceRooter<JSObject*> rooter(aCx, &childrenData); + uint32_t trailingNullCounter = 0; + + // This is not going to work for fission. Bug 1572084 for tracking it. + for (auto& child : aBrowsingContext->Children()) { + NullableRootedDictionary<CollectedData> data(aCx); + CollectFrameTreeData(aCx, child, data, aFunc); + if (data.IsNull()) { + childrenData.AppendElement(nullptr); + trailingNullCounter++; + continue; + } + JS::Rooted<JS::Value> jsval(aCx); + if (!ToJSValue(aCx, data.SetValue(), &jsval)) { + JS_ClearPendingException(aCx); + continue; + } + childrenData.AppendElement(&jsval.toObject()); + trailingNullCounter = 0; + } + + if (trailingNullCounter != childrenData.Length()) { + childrenData.TruncateLength(childrenData.Length() - trailingNullCounter); + aRetVal.SetValue().mChildren.Construct() = std::move(childrenData); + } +} + +/* static */ void SessionStoreUtils::CollectScrollPosition( + const GlobalObject& aGlobal, WindowProxyHolder& aWindow, + Nullable<CollectedData>& aRetVal) { + CollectFrameTreeData(aGlobal.Context(), aWindow.get(), aRetVal, + CollectCurrentScrollPosition); +} + +/* static */ void SessionStoreUtils::CollectFormData( + const GlobalObject& aGlobal, WindowProxyHolder& aWindow, + Nullable<CollectedData>& aRetVal) { + CollectFrameTreeData(aGlobal.Context(), aWindow.get(), aRetVal, + CollectCurrentFormData); +} + +/* static */ void SessionStoreUtils::ComposeInputData( + const nsTArray<CollectedInputDataValue>& aData, InputElementData& ret) { + nsTArray<int> selectedIndex, valueIdx; + nsTArray<nsString> id, selectVal, strVal, type; + nsTArray<bool> boolVal; + + for (const CollectedInputDataValue& data : aData) { + id.AppendElement(data.id); + type.AppendElement(data.type); + + if (data.value.is<mozilla::dom::CollectedNonMultipleSelectValue>()) { + valueIdx.AppendElement(selectVal.Length()); + selectedIndex.AppendElement( + data.value.as<mozilla::dom::CollectedNonMultipleSelectValue>() + .mSelectedIndex); + selectVal.AppendElement( + data.value.as<mozilla::dom::CollectedNonMultipleSelectValue>() + .mValue); + } else if (data.value.is<CopyableTArray<nsString>>()) { + // The first valueIdx is "index of the first string value" + valueIdx.AppendElement(strVal.Length()); + strVal.AppendElements(data.value.as<CopyableTArray<nsString>>()); + // The second valueIdx is "index of the last string value" + 1 + id.AppendElement(data.id); + type.AppendElement(data.type); + valueIdx.AppendElement(strVal.Length()); + } else if (data.value.is<nsString>()) { + valueIdx.AppendElement(strVal.Length()); + strVal.AppendElement(data.value.as<nsString>()); + } else if (data.type.EqualsLiteral("bool")) { + valueIdx.AppendElement(boolVal.Length()); + boolVal.AppendElement(data.value.as<bool>()); + } + } + + if (selectedIndex.Length() != 0) { + ret.mSelectedIndex.Construct(std::move(selectedIndex)); + } + if (valueIdx.Length() != 0) { + ret.mValueIdx.Construct(std::move(valueIdx)); + } + if (id.Length() != 0) { + ret.mId.Construct(std::move(id)); + } + if (selectVal.Length() != 0) { + ret.mSelectVal.Construct(std::move(selectVal)); + } + if (strVal.Length() != 0) { + ret.mStrVal.Construct(std::move(strVal)); + } + if (type.Length() != 0) { + ret.mType.Construct(std::move(type)); + } + if (boolVal.Length() != 0) { + ret.mBoolVal.Construct(std::move(boolVal)); + } +} + +already_AddRefed<nsISessionStoreRestoreData> +SessionStoreUtils::ConstructSessionStoreRestoreData( + const GlobalObject& aGlobal) { + nsCOMPtr<nsISessionStoreRestoreData> data = new SessionStoreRestoreData(); + return data.forget(); +} + +/* static */ +already_AddRefed<Promise> SessionStoreUtils::InitializeRestore( + const GlobalObject& aGlobal, CanonicalBrowsingContext& aContext, + nsISessionStoreRestoreData* aData, ErrorResult& aError) { + if (!mozilla::SessionHistoryInParent()) { + MOZ_CRASH("why were we called?"); + } + + MOZ_DIAGNOSTIC_ASSERT(aContext.IsTop()); + + MOZ_DIAGNOSTIC_ASSERT(aData); + nsCOMPtr<SessionStoreRestoreData> data = do_QueryInterface(aData); + aContext.SetRestoreData(data, aError); + if (aError.Failed()) { + return nullptr; + } + + nsCOMPtr<nsISHistory> shistory = aContext.GetSessionHistory(); + MOZ_DIAGNOSTIC_ASSERT(shistory); + shistory->ReloadCurrentEntry(); + + return aContext.GetRestorePromise(); +} + +/* static */ +void SessionStoreUtils::RestoreDocShellState( + nsIDocShell* aDocShell, const DocShellRestoreState& aState) { + if (aDocShell) { + nsCOMPtr<nsIURI> currentUri; + nsDocShell::Cast(aDocShell)->GetCurrentURI(getter_AddRefs(currentUri)); + if (aState.URI() && + (!currentUri || mozilla::net::SchemeIsAbout(currentUri))) { + aDocShell->SetCurrentURIForSessionStore(aState.URI()); + } + RestoreDocShellCapabilities(aDocShell, aState.docShellCaps()); + } +} + +/* static */ +already_AddRefed<Promise> SessionStoreUtils::RestoreDocShellState( + const GlobalObject& aGlobal, CanonicalBrowsingContext& aContext, + const nsACString& aURL, const nsCString& aDocShellCaps, + ErrorResult& aError) { + MOZ_RELEASE_ASSERT(mozilla::SessionHistoryInParent()); + MOZ_RELEASE_ASSERT(aContext.IsTop()); + + WindowGlobalParent* wgp = aContext.GetCurrentWindowGlobal(); + if (!wgp) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_DIAGNOSTIC_ASSERT(global); + + RefPtr<Promise> promise = Promise::Create(global, aError); + if (aError.Failed()) { + return nullptr; + } + + nsCOMPtr<nsIURI> uri; + if (!aURL.IsEmpty()) { + if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), aURL))) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + } + + bool allowJavascript = true; + for (const nsACString& token : + nsCCharSeparatedTokenizer(aDocShellCaps, ',').ToRange()) { + if (token.EqualsLiteral("Javascript")) { + allowJavascript = false; + } + } + + Unused << aContext.SetAllowJavascript(allowJavascript); + + DocShellRestoreState state = {uri, aDocShellCaps}; + + // TODO (anny): Investigate removing this roundtrip. + wgp->SendRestoreDocShellState(state)->Then( + GetMainThreadSerialEventTarget(), __func__, + [promise](void) { promise->MaybeResolveWithUndefined(); }, + [promise](void) { promise->MaybeRejectWithUndefined(); }); + + return promise.forget(); +} + +/* static */ +void SessionStoreUtils::RestoreSessionStorageFromParent( + const GlobalObject& aGlobal, const CanonicalBrowsingContext& aContext, + const Record<nsCString, Record<nsString, nsString>>& aSessionStorage) { + nsTArray<SSCacheCopy> cacheInitList; + for (const auto& originEntry : aSessionStorage.Entries()) { + nsCOMPtr<nsIPrincipal> storagePrincipal = + BasePrincipal::CreateContentPrincipal(originEntry.mKey); + + nsCString originKey; + nsresult rv = storagePrincipal->GetStorageOriginKey(originKey); + if (NS_FAILED(rv)) { + continue; + } + + SSCacheCopy& cacheInit = *cacheInitList.AppendElement(); + + cacheInit.originKey() = originKey; + PrincipalToPrincipalInfo(storagePrincipal, &cacheInit.principalInfo()); + + for (const auto& entry : originEntry.mValue.Entries()) { + SSSetItemInfo& setItemInfo = *cacheInit.data().AppendElement(); + setItemInfo.key() = entry.mKey; + setItemInfo.value() = entry.mValue; + } + } + + BackgroundSessionStorageManager::LoadData(aContext.Id(), cacheInitList); +} + +/* static */ +nsresult SessionStoreUtils::ConstructFormDataValues( + JSContext* aCx, const nsTArray<sessionstore::FormEntry>& aValues, + nsTArray<Record<nsString, OwningStringOrBooleanOrObject>::EntryType>& + aEntries, + bool aParseSessionData) { + using EntryType = Record<nsString, OwningStringOrBooleanOrObject>::EntryType; + + if (!aEntries.SetCapacity(aValues.Length(), fallible)) { + return NS_ERROR_FAILURE; + } + + for (const auto& value : aValues) { + EntryType* entry = aEntries.AppendElement(); + + using Type = sessionstore::FormEntryValue::Type; + switch (value.value().type()) { + case Type::TCheckbox: + entry->mValue.SetAsBoolean() = value.value().get_Checkbox().value(); + break; + case Type::TTextField: { + if (aParseSessionData && value.id() == u"sessionData"_ns) { + JS::Rooted<JS::Value> jsval(aCx); + const auto& fieldValue = value.value().get_TextField().value(); + if (!JS_ParseJSON(aCx, fieldValue.get(), fieldValue.Length(), + &jsval) || + !jsval.isObject()) { + return NS_ERROR_FAILURE; + } + entry->mValue.SetAsObject() = &jsval.toObject(); + } else { + entry->mValue.SetAsString() = value.value().get_TextField().value(); + } + break; + } + case Type::TFileList: { + CollectedFileListValue file; + file.mFileList = value.value().get_FileList().valueList().Clone(); + + JS::Rooted<JS::Value> jsval(aCx); + if (!ToJSValue(aCx, file, &jsval) || !jsval.isObject()) { + return NS_ERROR_FAILURE; + } + entry->mValue.SetAsObject() = &jsval.toObject(); + break; + } + case Type::TSingleSelect: { + CollectedNonMultipleSelectValue select; + select.mSelectedIndex = value.value().get_SingleSelect().index(); + select.mValue = value.value().get_SingleSelect().value(); + + JS::Rooted<JS::Value> jsval(aCx); + if (!ToJSValue(aCx, select, &jsval) || !jsval.isObject()) { + return NS_ERROR_FAILURE; + } + entry->mValue.SetAsObject() = &jsval.toObject(); + break; + } + case Type::TMultipleSelect: { + JS::Rooted<JS::Value> jsval(aCx); + if (!ToJSValue(aCx, value.value().get_MultipleSelect().valueList(), + &jsval) || + !jsval.isObject()) { + return NS_ERROR_FAILURE; + } + entry->mValue.SetAsObject() = &jsval.toObject(); + break; + } + case Type::TCustomElementTuple: { + nsCOMPtr<nsIGlobalObject> global; + JS::Rooted<JSObject*> globalObject(aCx, JS::CurrentGlobalOrNull(aCx)); + if (NS_WARN_IF(!globalObject)) { + break; + } + global = xpc::NativeGlobal(globalObject); + if (NS_WARN_IF(!global)) { + break; + } + + auto formState = + nsContentUtils::ExtractFormAssociatedCustomElementValue( + global, value.value().get_CustomElementTuple().state()); + auto formValue = + nsContentUtils::ExtractFormAssociatedCustomElementValue( + global, value.value().get_CustomElementTuple().value()); + MOZ_ASSERT(!formValue.IsNull() || !formState.IsNull(), + "Shouldn't be storing null values!"); + + CollectedCustomElementValue val; + val.mValue = formValue; + val.mState = formState; + JS::Rooted<JS::Value> jsval(aCx); + if (!ToJSValue(aCx, val, &jsval)) { + return NS_ERROR_FAILURE; + } + entry->mValue.SetAsObject() = &jsval.toObject(); + break; + } + default: + break; + } + + entry->mKey = value.id(); + } + + return NS_OK; +} + +static nsresult ConstructSessionStorageValue( + const nsTArray<SSSetItemInfo>& aValues, + Record<nsString, nsString>& aRecord) { + auto& entries = aRecord.Entries(); + for (const auto& value : aValues) { + auto entry = entries.AppendElement(); + entry->mKey = value.key(); + entry->mValue = value.value(); + } + + return NS_OK; +} + +/* static */ +nsresult SessionStoreUtils::ConstructSessionStorageValues( + CanonicalBrowsingContext* aBrowsingContext, + const nsTArray<SSCacheCopy>& aValues, + Record<nsCString, Record<nsString, nsString>>& aRecord) { + if (!aRecord.Entries().SetCapacity(aValues.Length(), fallible)) { + return NS_ERROR_FAILURE; + } + + for (const auto& value : aValues) { + auto storagePrincipal = PrincipalInfoToPrincipal(value.principalInfo()); + if (storagePrincipal.isErr()) { + continue; + } + + auto entry = aRecord.Entries().AppendElement(); + + if (!entry->mValue.Entries().SetCapacity(value.data().Length(), fallible)) { + return NS_ERROR_FAILURE; + } + + if (NS_FAILED(storagePrincipal.inspect()->GetOrigin(entry->mKey))) { + return NS_ERROR_FAILURE; + } + + ConstructSessionStorageValue(value.data(), entry->mValue); + } + + return NS_OK; +} + +/* static */ +bool SessionStoreUtils::CopyProperty(JSContext* aCx, JS::Handle<JSObject*> aDst, + JS::Handle<JSObject*> aSrc, + const nsAString& aName) { + JS::Rooted<JS::PropertyKey> name(aCx); + const char16_t* data; + size_t length = aName.GetData(&data); + + if (!JS_CharsToId(aCx, JS::TwoByteChars(data, length), &name)) { + return false; + } + + bool found = false; + if (!JS_HasPropertyById(aCx, aSrc, name, &found) || !found) { + return true; + } + + JS::Rooted<JS::Value> value(aCx); + if (!JS_GetPropertyById(aCx, aSrc, name, &value)) { + return false; + } + + if (value.isNullOrUndefined()) { + return true; + } + + return JS_DefinePropertyById(aCx, aDst, name, value, JSPROP_ENUMERATE); +} diff --git a/toolkit/components/sessionstore/SessionStoreUtils.h b/toolkit/components/sessionstore/SessionStoreUtils.h new file mode 100644 index 0000000000..e3028b0f29 --- /dev/null +++ b/toolkit/components/sessionstore/SessionStoreUtils.h @@ -0,0 +1,182 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SessionStoreUtils_h +#define mozilla_dom_SessionStoreUtils_h + +#include "mozilla/Attributes.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/SessionStoreUtilsBinding.h" +#include "SessionStoreData.h" +#include "SessionStoreRestoreData.h" + +class nsIDocument; +class nsGlobalWindowInner; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class CanonicalBrowsingContext; +class GlobalObject; +struct SSScrollPositionDict; +class SSCacheCopy; +class SSSetItemInfo; + +namespace sessionstore { +class DocShellRestoreState; +class FormData; +class FormEntry; +class StorageEntry; +} // namespace sessionstore + +class SessionStoreUtils { + public: + MOZ_CAN_RUN_SCRIPT + static void ForEachNonDynamicChildFrame( + const GlobalObject& aGlobal, WindowProxyHolder& aWindow, + SessionStoreUtilsFrameCallback& aCallback, ErrorResult& aRv); + + static already_AddRefed<nsISupports> AddDynamicFrameFilteredListener( + const GlobalObject& aGlobal, EventTarget& aTarget, const nsAString& aType, + JS::Handle<JS::Value> aListener, bool aUseCapture, bool aMozSystemGroup, + ErrorResult& aRv); + + static void RemoveDynamicFrameFilteredListener( + const GlobalObject& aGlobal, EventTarget& aTarget, const nsAString& aType, + nsISupports* aListener, bool aUseCapture, bool aMozSystemGroup, + ErrorResult& aRv); + + static void CollectDocShellCapabilities(const GlobalObject& aGlobal, + nsIDocShell* aDocShell, + nsCString& aRetVal); + + static void RestoreDocShellCapabilities( + nsIDocShell* aDocShell, const nsCString& aDisallowCapabilities); + static void RestoreDocShellCapabilities( + const GlobalObject& aGlobal, nsIDocShell* aDocShell, + const nsCString& aDisallowCapabilities) { + return RestoreDocShellCapabilities(aDocShell, aDisallowCapabilities); + } + + static void CollectScrollPosition(const GlobalObject& aGlobal, + WindowProxyHolder& aWindow, + Nullable<CollectedData>& aRetVal); + + static void RestoreScrollPosition(const GlobalObject& aGlobal, + nsGlobalWindowInner& aWindow, + const CollectedData& data); + static void RestoreScrollPosition(nsGlobalWindowInner& aWindow, + const nsCString& aScrollPosition); + + static uint32_t CollectFormData(Document* aDocument, + sessionstore::FormData& aFormData); + + /* + @param aDocument: DOMDocument instance to obtain form data for. + @param aGeneratedCount: the current number of XPath expressions in the + returned object. + */ + template <typename... ArgsT> + static void CollectFromTextAreaElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args); + template <typename... ArgsT> + static void CollectFromInputElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args); + template <typename... ArgsT> + static void CollectFromSelectElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args); + template <typename... ArgsT> + static void CollectFromFormAssociatedCustomElement(Document& aDocument, + uint16_t& aGeneratedCount, + ArgsT&&... args); + + static void CollectFormData(const GlobalObject& aGlobal, + WindowProxyHolder& aWindow, + Nullable<CollectedData>& aRetVal); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + static bool RestoreFormData(const GlobalObject& aGlobal, Document& aDocument, + const CollectedData& aData); + MOZ_CAN_RUN_SCRIPT static void RestoreFormData( + Document& aDocument, const nsString& aInnerHTML, + const nsTArray<SessionStoreRestoreData::Entry>& aEntries); + + static void ComposeInputData(const nsTArray<CollectedInputDataValue>& aData, + InputElementData& ret); + + MOZ_CAN_RUN_SCRIPT static already_AddRefed<nsISessionStoreRestoreData> + ConstructSessionStoreRestoreData(const GlobalObject& aGlobal); + + MOZ_CAN_RUN_SCRIPT static already_AddRefed<Promise> InitializeRestore( + const GlobalObject& aGlobal, CanonicalBrowsingContext& aContext, + nsISessionStoreRestoreData* aData, ErrorResult& aError); + + static void RestoreDocShellState( + nsIDocShell* aDocShell, const sessionstore::DocShellRestoreState& aState); + + static already_AddRefed<Promise> RestoreDocShellState( + const GlobalObject& aGlobal, CanonicalBrowsingContext& aContext, + const nsACString& aURL, const nsCString& aDocShellCaps, + ErrorResult& aError); + + static void RestoreSessionStorageFromParent( + const GlobalObject& aGlobal, const CanonicalBrowsingContext& aContext, + const Record<nsCString, Record<nsString, nsString>>& aSessionStorage); + + static nsresult ConstructFormDataValues( + JSContext* aCx, const nsTArray<sessionstore::FormEntry>& aValues, + nsTArray<Record<nsString, OwningStringOrBooleanOrObject>::EntryType>& + aEntries, + bool aParseSessionData = false); + + static nsresult ConstructSessionStorageValues( + CanonicalBrowsingContext* aBrowsingContext, + const nsTArray<SSCacheCopy>& aValues, + Record<nsCString, Record<nsString, nsString>>& aStorage); + + static bool CopyProperty(JSContext* aCx, JS::Handle<JSObject*> aDst, + JS::Handle<JSObject*> aSrc, const nsAString& aName); + + template <typename T> + static bool CopyChildren(JSContext* aCx, JS::Handle<JSObject*> aDst, + const nsTArray<RefPtr<T>>& aChildren) { + if (!aChildren.IsEmpty()) { + JS::Rooted<JSObject*> children( + aCx, JS::NewArrayObject(aCx, aChildren.Length())); + + for (const auto index : IntegerRange(aChildren.Length())) { + if (!aChildren[index]) { + continue; + } + + JS::Rooted<JSObject*> object(aCx); + aChildren[index]->ToJSON(aCx, &object); + + if (!JS_DefineElement(aCx, children, index, object, JSPROP_ENUMERATE)) { + return false; + } + } + + if (!JS_DefineProperty(aCx, aDst, "children", children, + JSPROP_ENUMERATE)) { + return false; + } + } + + return true; + } +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SessionStoreUtils_h diff --git a/toolkit/components/sessionstore/moz.build b/toolkit/components/sessionstore/moz.build new file mode 100644 index 0000000000..9e8d2eb45e --- /dev/null +++ b/toolkit/components/sessionstore/moz.build @@ -0,0 +1,55 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "BrowserSessionStore.h", + "SessionStoreChangeListener.h", + "SessionStoreChild.h", + "SessionStoreData.h", + "SessionStoreFormData.h", + "SessionStoreListener.h", + "SessionStoreMessageUtils.h", + "SessionStoreParent.h", + "SessionStoreRestoreData.h", + "SessionStoreScrollData.h", + "SessionStoreUtils.h", +] + +UNIFIED_SOURCES += [ + "BrowserSessionStore.cpp", + "RestoreTabContentObserver.cpp", + "SessionStoreChangeListener.cpp", + "SessionStoreChild.cpp", + "SessionStoreFormData.cpp", + "SessionStoreListener.cpp", + "SessionStoreParent.cpp", + "SessionStoreRestoreData.cpp", + "SessionStoreScrollData.cpp", + "SessionStoreUtils.cpp", +] + +EXTRA_JS_MODULES += [ + "SessionStoreFunctions.sys.mjs", +] + +XPIDL_MODULE = "sessionstore" + +XPIDL_SOURCES += [ + "nsISessionStoreRestoreData.idl", + "SessionStoreFunctions.idl", +] + +IPDL_SOURCES += [ + "PSessionStore.ipdl", + "SessionStoreTypes.ipdlh", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Session Restore") diff --git a/toolkit/components/sessionstore/nsISessionStoreRestoreData.idl b/toolkit/components/sessionstore/nsISessionStoreRestoreData.idl new file mode 100644 index 0000000000..eff070ef6a --- /dev/null +++ b/toolkit/components/sessionstore/nsISessionStoreRestoreData.idl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(cd9f33c5-460d-4bbf-a459-f375ca9566d8)] +interface nsISessionStoreRestoreData : nsISupports { + // Setters for form data. + attribute AUTF8String url; + attribute AString innerHTML; + + // Setters for scroll data. + attribute ACString scroll; + + // Methods for adding individual form fields which are called as the JS code + // finds them. + void addTextField(in boolean aIsXPath, in AString aIdOrXPath, + in AString aValue); + void addCheckbox(in boolean aIsXPath, in AString aIdOrXPath, + in boolean aValue); + void addFileList(in boolean aIsXPath, in AString aIdOrXPath, in AString aType, + in Array<AString> aFileList); + void addSingleSelect(in boolean aIsXPath, in AString aIdOrXPath, + in unsigned long aSelectedIndex, in AString aValue); + void addMultipleSelect(in boolean aIsXPath, in AString aIdOrXPath, + in Array<AString> aValues); + void addCustomElement(in boolean aIsXPath, in AString aIdOrXPath, + in jsval aValue, in jsval aState); + + // Add a child data object to our children list. + void addChild(in nsISessionStoreRestoreData aChild, in unsigned long aIndex); +}; |