Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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();
}
}