зеркало из https://github.com/mozilla/gecko-dev.git
1042 строки
33 KiB
JavaScript
1042 строки
33 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["ContentSessionStore"];
|
|
|
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
|
ChromeUtils.import("resource://gre/modules/Timer.jsm", this);
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
|
|
|
|
ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
|
|
"resource://gre/modules/TelemetryStopwatch.jsm");
|
|
|
|
function debug(msg) {
|
|
Services.console.logStringMessage("SessionStoreContent: " + msg);
|
|
}
|
|
|
|
ChromeUtils.defineModuleGetter(this, "FormData",
|
|
"resource://gre/modules/FormData.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(this, "ContentRestore",
|
|
"resource:///modules/sessionstore/ContentRestore.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "DocShellCapabilities",
|
|
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "ScrollPosition",
|
|
"resource://gre/modules/ScrollPosition.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "SessionHistory",
|
|
"resource://gre/modules/sessionstore/SessionHistory.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "SessionStorage",
|
|
"resource:///modules/sessionstore/SessionStorage.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(this, "Utils",
|
|
"resource://gre/modules/sessionstore/Utils.jsm");
|
|
const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
|
|
.getService(Ci.nsISessionStoreUtils);
|
|
|
|
// A bound to the size of data to store for DOM Storage.
|
|
const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit";
|
|
|
|
// 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.
|
|
const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates";
|
|
|
|
const PREF_INTERVAL = "browser.sessionstore.interval";
|
|
|
|
const kNoIndex = Number.MAX_SAFE_INTEGER;
|
|
const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
|
|
|
|
/**
|
|
* A function that will recursively call |cb| to collect data for all
|
|
* non-dynamic frames in the current frame/docShell tree.
|
|
*/
|
|
function mapFrameTree(mm, callback) {
|
|
let [data] = Utils.mapFrameTree(mm.content, callback);
|
|
return data;
|
|
}
|
|
|
|
class Handler {
|
|
constructor(store) {
|
|
this.store = store;
|
|
}
|
|
|
|
get contentRestore() {
|
|
return this.store.contentRestore;
|
|
}
|
|
|
|
get contentRestoreInitialized() {
|
|
return this.store.contentRestoreInitialized;
|
|
}
|
|
|
|
get mm() {
|
|
return this.store.mm;
|
|
}
|
|
|
|
get messageQueue() {
|
|
return this.store.messageQueue;
|
|
}
|
|
|
|
get stateChangeNotifier() {
|
|
return this.store.stateChangeNotifier;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listens for state change notifcations from webProgress and notifies each
|
|
* registered observer for either the start of a page load, or its completion.
|
|
*/
|
|
class StateChangeNotifier extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
this._observers = new Set();
|
|
let ifreq = this.mm.docShell.QueryInterface(Ci.nsIInterfaceRequestor);
|
|
let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
|
|
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
|
|
}
|
|
|
|
/**
|
|
* Adds a given observer |obs| to the set of observers that will be notified
|
|
* when when a new document starts or finishes loading.
|
|
*
|
|
* @param obs (object)
|
|
*/
|
|
addObserver(obs) {
|
|
this._observers.add(obs);
|
|
}
|
|
|
|
/**
|
|
* Notifies all observers that implement the given |method|.
|
|
*
|
|
* @param method (string)
|
|
*/
|
|
notifyObservers(method) {
|
|
for (let obs of this._observers) {
|
|
if (typeof obs[method] == "function") {
|
|
obs[method]();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see nsIWebProgressListener.onStateChange
|
|
*/
|
|
onStateChange(webProgress, request, stateFlags, status) {
|
|
// Ignore state changes for subframes because we're only interested in the
|
|
// top-document starting or stopping its load.
|
|
if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) {
|
|
return;
|
|
}
|
|
|
|
// 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 (!this.mm.docShell.hasLoadedNonBlankURI) {
|
|
return;
|
|
}
|
|
|
|
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
|
|
this.notifyObservers("onPageLoadStarted");
|
|
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
|
|
this.notifyObservers("onPageLoadCompleted");
|
|
}
|
|
}
|
|
}
|
|
StateChangeNotifier.prototype.QueryInterface =
|
|
ChromeUtils.generateQI([Ci.nsIWebProgressListener,
|
|
Ci.nsISupportsWeakReference]);
|
|
|
|
/**
|
|
* Listens for and handles content events that we need for the
|
|
* session store service to be notified of state changes in content.
|
|
*/
|
|
class EventListener extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
ssu.addDynamicFrameFilteredListener(this.mm, "load", this, true);
|
|
}
|
|
|
|
handleEvent(event) {
|
|
let {content} = this.mm;
|
|
|
|
// Ignore load events from subframes.
|
|
if (event.target != content.document) {
|
|
return;
|
|
}
|
|
|
|
if (content.document.documentURI.startsWith("about:reader")) {
|
|
if (event.type == "load" &&
|
|
!content.document.body.classList.contains("loaded")) {
|
|
// Don't restore the scroll position of an about:reader page at this
|
|
// point; listen for the custom event dispatched from AboutReader.jsm.
|
|
content.addEventListener("AboutReaderContentReady", this);
|
|
return;
|
|
}
|
|
|
|
content.removeEventListener("AboutReaderContentReady", this);
|
|
}
|
|
|
|
if (this.contentRestoreInitialized) {
|
|
// Restore the form data and scroll position. If we're not currently
|
|
// restoring a tab state then this call will simply be a noop.
|
|
this.contentRestore.restoreDocument();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listens for changes to the session history. Whenever the user navigates
|
|
* we will collect URLs and everything belonging to session history.
|
|
*
|
|
* Causes a SessionStore:update message to be sent that contains the current
|
|
* session history.
|
|
*
|
|
* Example:
|
|
* {entries: [{url: "about:mozilla", ...}, ...], index: 1}
|
|
*/
|
|
class SessionHistoryListener extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
this._fromIdx = kNoIndex;
|
|
|
|
|
|
// The state change observer is needed to handle initial subframe loads.
|
|
// It will redundantly invalidate with the SHistoryListener in some cases
|
|
// but these invalidations are very cheap.
|
|
this.stateChangeNotifier.addObserver(this);
|
|
|
|
// By adding the SHistoryListener immediately, we will unfortunately be
|
|
// notified of every history entry as the tab is restored. We don't bother
|
|
// waiting to add the listener later because these notifications are cheap.
|
|
// We will likely only collect once since we are batching collection on
|
|
// a delay.
|
|
this.mm.docShell.QueryInterface(Ci.nsIWebNavigation)
|
|
.sessionHistory.legacySHistory.addSHistoryListener(this);
|
|
|
|
// Collect data if we start with a non-empty shistory.
|
|
if (!SessionHistory.isEmpty(this.mm.docShell)) {
|
|
this.collect();
|
|
// When a tab is detached from the window, for the new window there is a
|
|
// new SessionHistoryListener created. Normally it is empty at this point
|
|
// but in a test env. the initial about:blank might have a children in which
|
|
// case we fire off a history message here with about:blank in it. If we
|
|
// don't do it ASAP then there is going to be a browser swap and the parent
|
|
// will be all confused by that message.
|
|
this.messageQueue.send();
|
|
}
|
|
|
|
// Listen for page title changes.
|
|
this.mm.addEventListener("DOMTitleChanged", this);
|
|
}
|
|
|
|
uninit() {
|
|
let sessionHistory = this.mm.docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
|
|
if (sessionHistory) {
|
|
sessionHistory.legacySHistory.removeSHistoryListener(this);
|
|
}
|
|
}
|
|
|
|
collect() {
|
|
// We want to send down a historychange even for full collects in case our
|
|
// session history is a partial session history, in which case we don't have
|
|
// enough information for a full update. collectFrom(-1) tells the collect
|
|
// function to collect all data avaliable in this process.
|
|
if (this.mm.docShell) {
|
|
this.collectFrom(-1);
|
|
}
|
|
}
|
|
|
|
// History can grow relatively big with the nested elements, so if we don't have to, we
|
|
// don't want to send the entire history all the time. For a simple optimization
|
|
// we keep track of the smallest index from after any change has occured and we just send
|
|
// the elements from that index. If something more complicated happens we just clear it
|
|
// and send the entire history. We always send the additional info like the current selected
|
|
// index (so for going back and forth between history entries we set the index to kLastIndex
|
|
// if nothing else changed send an empty array and the additonal info like the selected index)
|
|
collectFrom(idx) {
|
|
if (this._fromIdx <= idx) {
|
|
// If we already know that we need to update history fromn index N we can ignore any changes
|
|
// tha happened with an element with index larger than N.
|
|
// Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything
|
|
// here, and in case of navigation in the history back and forth we use kLastIndex which ignores
|
|
// only the subsequent navigations, but not any new elements added.
|
|
return;
|
|
}
|
|
|
|
this._fromIdx = idx;
|
|
this.messageQueue.push("historychange", () => {
|
|
if (this._fromIdx === kNoIndex) {
|
|
return null;
|
|
}
|
|
|
|
let history = SessionHistory.collect(this.mm.docShell, this._fromIdx);
|
|
this._fromIdx = kNoIndex;
|
|
return history;
|
|
});
|
|
}
|
|
|
|
handleEvent(event) {
|
|
this.collect();
|
|
}
|
|
|
|
onPageLoadCompleted() {
|
|
this.collect();
|
|
}
|
|
|
|
onPageLoadStarted() {
|
|
this.collect();
|
|
}
|
|
|
|
OnHistoryNewEntry(newURI, oldIndex) {
|
|
// We ought to collect the previously current entry as well, see bug 1350567.
|
|
this.collectFrom(oldIndex);
|
|
}
|
|
|
|
OnHistoryGoBack(backURI) {
|
|
// We ought to collect the previously current entry as well, see bug 1350567.
|
|
this.collectFrom(kLastIndex);
|
|
return true;
|
|
}
|
|
|
|
OnHistoryGoForward(forwardURI) {
|
|
// We ought to collect the previously current entry as well, see bug 1350567.
|
|
this.collectFrom(kLastIndex);
|
|
return true;
|
|
}
|
|
|
|
OnHistoryGotoIndex(index, gotoURI) {
|
|
// We ought to collect the previously current entry as well, see bug 1350567.
|
|
this.collectFrom(kLastIndex);
|
|
return true;
|
|
}
|
|
|
|
OnHistoryPurge(numEntries) {
|
|
this.collect();
|
|
return true;
|
|
}
|
|
|
|
OnHistoryReload(reloadURI, reloadFlags) {
|
|
this.collect();
|
|
return true;
|
|
}
|
|
|
|
OnHistoryReplaceEntry(index) {
|
|
this.collect();
|
|
}
|
|
|
|
OnLengthChanged(aCount) {
|
|
// Ignore, the method is implemented so that XPConnect doesn't throw!
|
|
}
|
|
|
|
OnIndexChanged(aIndex) {
|
|
// Ignore, the method is implemented so that XPConnect doesn't throw!
|
|
}
|
|
}
|
|
SessionHistoryListener.prototype.QueryInterface =
|
|
ChromeUtils.generateQI([Ci.nsISHistoryListener,
|
|
Ci.nsISupportsWeakReference]);
|
|
|
|
/**
|
|
* Listens for scroll position changes. Whenever the user scrolls the top-most
|
|
* frame we update the scroll position and will restore it when requested.
|
|
*
|
|
* Causes a SessionStore:update message to be sent that contains the current
|
|
* scroll positions as a tree of strings. If no frame of the whole frame tree
|
|
* is scrolled this will return null so that we don't tack a property onto
|
|
* the tabData object in the parent process.
|
|
*
|
|
* Example:
|
|
* {scroll: "100,100", children: [null, null, {scroll: "200,200"}]}
|
|
*/
|
|
class ScrollPositionListener extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
ssu.addDynamicFrameFilteredListener(this.mm, "scroll", this, false);
|
|
this.stateChangeNotifier.addObserver(this);
|
|
}
|
|
|
|
handleEvent() {
|
|
this.messageQueue.push("scroll", () => this.collect());
|
|
}
|
|
|
|
onPageLoadCompleted() {
|
|
this.messageQueue.push("scroll", () => this.collect());
|
|
}
|
|
|
|
onPageLoadStarted() {
|
|
this.messageQueue.push("scroll", () => null);
|
|
}
|
|
|
|
collect() {
|
|
return mapFrameTree(this.mm, ScrollPosition.collect);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listens for changes to input elements. Whenever the value of an input
|
|
* element changes we will re-collect data for the current frame tree and send
|
|
* a message to the parent process.
|
|
*
|
|
* Causes a SessionStore:update message to be sent that contains the form data
|
|
* for all reachable frames.
|
|
*
|
|
* Example:
|
|
* {
|
|
* formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}},
|
|
* children: [
|
|
* null,
|
|
* {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}}
|
|
* ]
|
|
* }
|
|
*/
|
|
class FormDataListener extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
ssu.addDynamicFrameFilteredListener(this.mm, "input", this, true);
|
|
this.stateChangeNotifier.addObserver(this);
|
|
}
|
|
|
|
handleEvent() {
|
|
this.messageQueue.push("formdata", () => this.collect());
|
|
}
|
|
|
|
onPageLoadStarted() {
|
|
this.messageQueue.push("formdata", () => null);
|
|
}
|
|
|
|
collect() {
|
|
return mapFrameTree(this.mm, FormData.collect);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listens for changes to docShell capabilities. Whenever a new load is started
|
|
* we need to re-check the list of capabilities and send message when it has
|
|
* changed.
|
|
*
|
|
* Causes a SessionStore:update message to be sent that contains the currently
|
|
* disabled docShell capabilities (all nsIDocShell.allow* properties set to
|
|
* false) as a string - i.e. capability names separate by commas.
|
|
*/
|
|
class DocShellCapabilitiesListener extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
/**
|
|
* This field is used to compare the last docShell capabilities to the ones
|
|
* that have just been collected. If nothing changed we won't send a message.
|
|
*/
|
|
this._latestCapabilities = "";
|
|
|
|
this.stateChangeNotifier.addObserver(this);
|
|
}
|
|
|
|
onPageLoadStarted() {
|
|
// The order of docShell capabilities cannot change while we're running
|
|
// so calling join() without sorting before is totally sufficient.
|
|
let caps = DocShellCapabilities.collect(this.mm.docShell).join(",");
|
|
|
|
// Send new data only when the capability list changes.
|
|
if (caps != this._latestCapabilities) {
|
|
this._latestCapabilities = caps;
|
|
this.messageQueue.push("disallow", () => caps || null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listens for changes to the DOMSessionStorage. Whenever new keys are added,
|
|
* existing ones removed or changed, or the storage is cleared we will send a
|
|
* message to the parent process containing up-to-date sessionStorage data.
|
|
*
|
|
* Causes a SessionStore:update message to be sent that contains the current
|
|
* DOMSessionStorage contents. The data is a nested object using host names
|
|
* as keys and per-host DOMSessionStorage data as values.
|
|
*/
|
|
class SessionStorageListener extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
// We don't want to send all the session storage data for all the frames
|
|
// for every change. So if only a few value changed we send them over as
|
|
// a "storagechange" event. If however for some reason before we send these
|
|
// changes we have to send over the entire sessions storage data, we just
|
|
// reset these changes.
|
|
this._changes = undefined;
|
|
|
|
// The event listener waiting for MozSessionStorageChanged events.
|
|
this._listener = null;
|
|
|
|
Services.obs.addObserver(this, "browser:purge-domain-data");
|
|
this.stateChangeNotifier.addObserver(this);
|
|
this.resetEventListener();
|
|
}
|
|
|
|
uninit() {
|
|
Services.obs.removeObserver(this, "browser:purge-domain-data");
|
|
}
|
|
|
|
observe() {
|
|
// Collect data on the next tick so that any other observer
|
|
// that needs to purge data can do its work first.
|
|
setTimeoutWithTarget(() => this.collect(), 0, this.mm.tabEventTarget);
|
|
}
|
|
|
|
resetChanges() {
|
|
this._changes = undefined;
|
|
}
|
|
|
|
resetEventListener() {
|
|
if (!this._listener) {
|
|
this._listener =
|
|
ssu.addDynamicFrameFilteredListener(this.mm, "MozSessionStorageChanged",
|
|
this, true);
|
|
}
|
|
}
|
|
|
|
removeEventListener() {
|
|
ssu.removeDynamicFrameFilteredListener(this.mm, "MozSessionStorageChanged",
|
|
this._listener, true);
|
|
this._listener = null;
|
|
}
|
|
|
|
handleEvent(event) {
|
|
if (!this.mm.docShell) {
|
|
return;
|
|
}
|
|
|
|
let {content} = this.mm;
|
|
|
|
// How much data does DOMSessionStorage contain?
|
|
let usage = content.windowUtils.getStorageUsage(event.storageArea);
|
|
|
|
// Don't store any data if we exceed the limit. Wipe any data we previously
|
|
// collected so that we don't confuse websites with partial state.
|
|
if (usage > Services.prefs.getIntPref(DOM_STORAGE_LIMIT_PREF)) {
|
|
this.messageQueue.push("storage", () => null);
|
|
this.removeEventListener();
|
|
this.resetChanges();
|
|
return;
|
|
}
|
|
|
|
let {url, key, newValue} = event;
|
|
let uri = Services.io.newURI(url);
|
|
let domain = uri.prePath;
|
|
if (!this._changes) {
|
|
this._changes = {};
|
|
}
|
|
if (!this._changes[domain]) {
|
|
this._changes[domain] = {};
|
|
}
|
|
this._changes[domain][key] = newValue;
|
|
|
|
this.messageQueue.push("storagechange", () => {
|
|
let tmp = this._changes;
|
|
// If there were multiple changes we send them merged.
|
|
// First one will collect all the changes the rest of
|
|
// these messages will be ignored.
|
|
this.resetChanges();
|
|
return tmp;
|
|
});
|
|
}
|
|
|
|
collect() {
|
|
if (!this.mm.docShell) {
|
|
return;
|
|
}
|
|
|
|
let {content} = this.mm;
|
|
|
|
// We need the entire session storage, let's reset the pending individual change
|
|
// messages.
|
|
this.resetChanges();
|
|
|
|
this.messageQueue.push("storage", () => SessionStorage.collect(content));
|
|
}
|
|
|
|
onPageLoadCompleted() {
|
|
this.collect();
|
|
}
|
|
|
|
onPageLoadStarted() {
|
|
this.resetEventListener();
|
|
this.collect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listen for changes to the privacy status of the tab.
|
|
* By definition, tabs start in non-private mode.
|
|
*
|
|
* Causes a SessionStore:update message to be sent for
|
|
* field "isPrivate". This message contains
|
|
* |true| if the tab is now private
|
|
* |null| if the tab is now public - the field is therefore
|
|
* not saved.
|
|
*/
|
|
class PrivacyListener extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
this.mm.docShell.addWeakPrivacyTransitionObserver(this);
|
|
|
|
// Check that value at startup as it might have
|
|
// been set before the frame script was loaded.
|
|
if (this.mm.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) {
|
|
this.messageQueue.push("isPrivate", () => true);
|
|
}
|
|
}
|
|
|
|
// Ci.nsIPrivacyTransitionObserver
|
|
privateModeChanged(enabled) {
|
|
this.messageQueue.push("isPrivate", () => enabled || null);
|
|
}
|
|
}
|
|
PrivacyListener.prototype.QueryInterface =
|
|
ChromeUtils.generateQI([Ci.nsIPrivacyTransitionObserver,
|
|
Ci.nsISupportsWeakReference]);
|
|
|
|
/**
|
|
* A message queue that takes collected data and will take care of sending it
|
|
* to the chrome process. It allows flushing using synchronous messages and
|
|
* takes care of any race conditions that might occur because of that. Changes
|
|
* will be batched if they're pushed in quick succession to avoid a message
|
|
* flood.
|
|
*/
|
|
class MessageQueue extends Handler {
|
|
constructor(store) {
|
|
super(store);
|
|
|
|
/**
|
|
* A map (string -> lazy fn) holding lazy closures of all queued data
|
|
* collection routines. These functions will return data collected from the
|
|
* docShell.
|
|
*/
|
|
this._data = new Map();
|
|
|
|
/**
|
|
* The delay (in ms) used to delay sending changes after data has been
|
|
* invalidated.
|
|
*/
|
|
this.BATCH_DELAY_MS = 1000;
|
|
|
|
/**
|
|
* The minimum idle period (in ms) we need for sending data to chrome process.
|
|
*/
|
|
this.NEEDED_IDLE_PERIOD_MS = 5;
|
|
|
|
/**
|
|
* Timeout for waiting an idle period to send data. We will set this from
|
|
* the pref "browser.sessionstore.interval".
|
|
*/
|
|
this._timeoutWaitIdlePeriodMs = null;
|
|
|
|
/**
|
|
* The current timeout ID, null if there is no queue data. We use timeouts
|
|
* to damp a flood of data changes and send lots of changes as one batch.
|
|
*/
|
|
this._timeout = null;
|
|
|
|
/**
|
|
* Whether or not sending batched messages on a timer is disabled. This should
|
|
* only be used for debugging or testing. If you need to access this value,
|
|
* you should probably use the timeoutDisabled getter.
|
|
*/
|
|
this._timeoutDisabled = false;
|
|
|
|
/**
|
|
* True if there is already a send pending idle dispatch, set to prevent
|
|
* scheduling more than one. If false there may or may not be one scheduled.
|
|
*/
|
|
this._idleScheduled = false;
|
|
|
|
|
|
this.timeoutDisabled =
|
|
Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
|
|
this._timeoutWaitIdlePeriodMs =
|
|
Services.prefs.getIntPref(PREF_INTERVAL);
|
|
|
|
Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this);
|
|
Services.prefs.addObserver(PREF_INTERVAL, this);
|
|
}
|
|
|
|
/**
|
|
* True if batched messages are not being fired on a timer. This should only
|
|
* ever be true when debugging or during tests.
|
|
*/
|
|
get timeoutDisabled() {
|
|
return this._timeoutDisabled;
|
|
}
|
|
|
|
/**
|
|
* Disables sending batched messages on a timer. Also cancels any pending
|
|
* timers.
|
|
*/
|
|
set timeoutDisabled(val) {
|
|
this._timeoutDisabled = val;
|
|
|
|
if (val && this._timeout) {
|
|
clearTimeout(this._timeout);
|
|
this._timeout = null;
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
uninit() {
|
|
Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this);
|
|
Services.prefs.removeObserver(PREF_INTERVAL, this);
|
|
this.cleanupTimers();
|
|
}
|
|
|
|
/**
|
|
* Cleanup pending idle callback and timer.
|
|
*/
|
|
cleanupTimers() {
|
|
this._idleScheduled = false;
|
|
if (this._timeout) {
|
|
clearTimeout(this._timeout);
|
|
this._timeout = null;
|
|
}
|
|
}
|
|
|
|
observe(subject, topic, data) {
|
|
if (topic == "nsPref:changed") {
|
|
switch (data) {
|
|
case TIMEOUT_DISABLED_PREF:
|
|
this.timeoutDisabled =
|
|
Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
|
|
break;
|
|
case PREF_INTERVAL:
|
|
this._timeoutWaitIdlePeriodMs =
|
|
Services.prefs.getIntPref(PREF_INTERVAL);
|
|
break;
|
|
default:
|
|
debug("received unknown message '" + data + "'");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pushes a given |value| onto the queue. The given |key| represents the type
|
|
* of data that is stored and can override data that has been queued before
|
|
* but has not been sent to the parent process, yet.
|
|
*
|
|
* @param key (string)
|
|
* A unique identifier specific to the type of data this is passed.
|
|
* @param fn (function)
|
|
* A function that returns the value that will be sent to the parent
|
|
* process.
|
|
*/
|
|
push(key, fn) {
|
|
this._data.set(key, fn);
|
|
|
|
if (!this._timeout && !this._timeoutDisabled) {
|
|
// Wait a little before sending the message to batch multiple changes.
|
|
this._timeout = setTimeoutWithTarget(
|
|
() => this.sendWhenIdle(), this.BATCH_DELAY_MS, this.mm.tabEventTarget);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends queued data when the remaining idle time is enough or waiting too
|
|
* long; otherwise, request an idle time again. If the |deadline| is not
|
|
* given, this function is going to schedule the first request.
|
|
*
|
|
* @param deadline (object)
|
|
* An IdleDeadline object passed by idleDispatch().
|
|
*/
|
|
sendWhenIdle(deadline) {
|
|
if (!this.mm.content) {
|
|
// The frameloader is being torn down. Nothing more to do.
|
|
return;
|
|
}
|
|
|
|
if (deadline) {
|
|
if (deadline.didTimeout || deadline.timeRemaining() > this.NEEDED_IDLE_PERIOD_MS) {
|
|
this.send();
|
|
return;
|
|
}
|
|
} else if (this._idleScheduled) {
|
|
// Bail out if there's a pending run.
|
|
return;
|
|
}
|
|
ChromeUtils.idleDispatch((deadline_) => this.sendWhenIdle(deadline_),
|
|
{timeout: this._timeoutWaitIdlePeriodMs});
|
|
this._idleScheduled = true;
|
|
}
|
|
|
|
/**
|
|
* Sends queued data to the chrome process.
|
|
*
|
|
* @param options (object)
|
|
* {flushID: 123} to specify that this is a flush
|
|
* {isFinal: true} to signal this is the final message sent on unload
|
|
*/
|
|
send(options = {}) {
|
|
// Looks like we have been called off a timeout after the tab has been
|
|
// closed. The docShell is gone now and we can just return here as there
|
|
// is nothing to do.
|
|
if (!this.mm.docShell) {
|
|
return;
|
|
}
|
|
|
|
this.cleanupTimers();
|
|
|
|
let flushID = (options && options.flushID) || 0;
|
|
let histID = "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_MS";
|
|
|
|
let data = {};
|
|
for (let [key, func] of this._data) {
|
|
if (key != "isPrivate") {
|
|
TelemetryStopwatch.startKeyed(histID, key);
|
|
}
|
|
|
|
let value = func();
|
|
|
|
if (key != "isPrivate") {
|
|
TelemetryStopwatch.finishKeyed(histID, key);
|
|
}
|
|
|
|
if (value || (key != "storagechange" && key != "historychange")) {
|
|
data[key] = value;
|
|
}
|
|
}
|
|
|
|
this._data.clear();
|
|
|
|
try {
|
|
// Send all data to the parent process.
|
|
this.mm.sendAsyncMessage("SessionStore:update", {
|
|
data, flushID,
|
|
isFinal: options.isFinal || false,
|
|
epoch: this.store.epoch,
|
|
});
|
|
} catch (ex) {
|
|
if (ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) {
|
|
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM").add(1);
|
|
this.mm.sendAsyncMessage("SessionStore:error");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listens for and handles messages sent by the session store service.
|
|
*/
|
|
const MESSAGES = [
|
|
"SessionStore:restoreHistory",
|
|
"SessionStore:restoreTabContent",
|
|
"SessionStore:resetRestore",
|
|
"SessionStore:flush",
|
|
"SessionStore:becomeActiveProcess",
|
|
];
|
|
|
|
class ContentSessionStore {
|
|
constructor(mm) {
|
|
this.mm = mm;
|
|
this.messageQueue = new MessageQueue(this);
|
|
this.stateChangeNotifier = new StateChangeNotifier(this);
|
|
|
|
this.epoch = 0;
|
|
|
|
this.contentRestoreInitialized = false;
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "contentRestore",
|
|
() => {
|
|
this.contentRestoreInitialized = true;
|
|
return new ContentRestore(mm);
|
|
});
|
|
|
|
this.handlers = [
|
|
new EventListener(this),
|
|
new FormDataListener(this),
|
|
new SessionHistoryListener(this),
|
|
new SessionStorageListener(this),
|
|
new ScrollPositionListener(this),
|
|
new DocShellCapabilitiesListener(this),
|
|
new PrivacyListener(this),
|
|
this.stateChangeNotifier,
|
|
this.messageQueue,
|
|
];
|
|
|
|
MESSAGES.forEach(m => mm.addMessageListener(m, this));
|
|
|
|
// If we're browsing from the tab crashed UI to a blacklisted URI that keeps
|
|
// this browser non-remote, we'll handle that in a pagehide event.
|
|
mm.addEventListener("pagehide", this);
|
|
mm.addEventListener("unload", this);
|
|
}
|
|
|
|
receiveMessage({name, data}) {
|
|
// The docShell might be gone. Don't process messages,
|
|
// that will just lead to errors anyway.
|
|
if (!this.mm.docShell) {
|
|
return;
|
|
}
|
|
|
|
// A fresh tab always starts with epoch=0. The parent has the ability to
|
|
// override that to signal a new era in this tab's life. This enables it
|
|
// to ignore async messages that were already sent but not yet received
|
|
// and would otherwise confuse the internal tab state.
|
|
if (data.epoch && data.epoch != this.epoch) {
|
|
this.epoch = data.epoch;
|
|
}
|
|
|
|
switch (name) {
|
|
case "SessionStore:restoreHistory":
|
|
this.restoreHistory(data);
|
|
break;
|
|
case "SessionStore:restoreTabContent":
|
|
if (data.isRemotenessUpdate) {
|
|
let histogram = Services.telemetry.getKeyedHistogramById("FX_TAB_REMOTE_NAVIGATION_DELAY_MS");
|
|
histogram.add("SessionStore:restoreTabContent",
|
|
Services.telemetry.msSystemNow() - data.requestTime);
|
|
}
|
|
this.restoreTabContent(data);
|
|
break;
|
|
case "SessionStore:resetRestore":
|
|
this.contentRestore.resetRestore();
|
|
break;
|
|
case "SessionStore:flush":
|
|
this.flush(data);
|
|
break;
|
|
case "SessionStore:becomeActiveProcess":
|
|
SessionHistoryListener.collect();
|
|
break;
|
|
default:
|
|
debug("received unknown message '" + name + "'");
|
|
break;
|
|
}
|
|
}
|
|
|
|
restoreHistory({epoch, tabData, loadArguments, isRemotenessUpdate}) {
|
|
this.contentRestore.restoreHistory(tabData, loadArguments, {
|
|
// Note: The callbacks passed here will only be used when a load starts
|
|
// that was not initiated by sessionstore itself. This can happen when
|
|
// some code calls browser.loadURI() or browser.reload() on a pending
|
|
// browser/tab.
|
|
|
|
onLoadStarted: () => {
|
|
// Notify the parent that the tab is no longer pending.
|
|
this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch});
|
|
},
|
|
|
|
onLoadFinished: () => {
|
|
// Tell SessionStore.jsm that it may want to restore some more tabs,
|
|
// since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
|
|
this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch});
|
|
}
|
|
});
|
|
|
|
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
|
|
// For non-remote tabs, when restoreHistory finishes, we send a synchronous
|
|
// message to SessionStore.jsm so that it can run SSTabRestoring. Users of
|
|
// SSTabRestoring seem to get confused if chrome and content are out of
|
|
// sync about the state of the restore (particularly regarding
|
|
// docShell.currentURI). Using a synchronous message is the easiest way
|
|
// to temporarily synchronize them.
|
|
//
|
|
// For remote tabs, because all nsIWebProgress notifications are sent
|
|
// asynchronously using messages, we get the same-order guarantees of the
|
|
// message manager, and can use an async message.
|
|
this.mm.sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate});
|
|
} else {
|
|
this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate});
|
|
}
|
|
}
|
|
|
|
restoreTabContent({loadArguments, isRemotenessUpdate, reason}) {
|
|
let epoch = this.epoch;
|
|
|
|
// We need to pass the value of didStartLoad back to SessionStore.jsm.
|
|
let didStartLoad = this.contentRestore.restoreTabContent(loadArguments, isRemotenessUpdate, () => {
|
|
// Tell SessionStore.jsm that it may want to restore some more tabs,
|
|
// since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
|
|
this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
|
|
});
|
|
|
|
this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", {
|
|
epoch, isRemotenessUpdate, reason,
|
|
});
|
|
|
|
if (!didStartLoad) {
|
|
// Pretend that the load succeeded so that event handlers fire correctly.
|
|
this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
|
|
}
|
|
}
|
|
|
|
flush({id}) {
|
|
// Flush the message queue, send the latest updates.
|
|
this.messageQueue.send({flushID: id});
|
|
}
|
|
|
|
handleEvent(event) {
|
|
if (event.type == "pagehide") {
|
|
this.handleRevivedTab();
|
|
} else if (event.type == "unload") {
|
|
this.onUnload();
|
|
}
|
|
}
|
|
|
|
onUnload() {
|
|
// Upon frameLoader destruction, send a final update message to
|
|
// the parent and flush all data currently held in the child.
|
|
this.messageQueue.send({isFinal: true});
|
|
|
|
// If we're browsing from the tab crashed UI to a URI that causes the tab
|
|
// to go remote again, we catch this in the unload event handler, because
|
|
// swapping out the non-remote browser for a remote one in
|
|
// tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide
|
|
// event to be fired.
|
|
this.handleRevivedTab();
|
|
|
|
for (let handler of this.handlers) {
|
|
if (handler.uninit) {
|
|
handler.uninit();
|
|
}
|
|
}
|
|
|
|
if (this.contentRestoreInitialized) {
|
|
// Remove progress listeners.
|
|
this.contentRestore.resetRestore();
|
|
}
|
|
|
|
// We don't need to take care of any StateChangeNotifier observers as they
|
|
// will die with the content script. The same goes for the privacy transition
|
|
// observer that will die with the docShell when the tab is closed.
|
|
}
|
|
|
|
handleRevivedTab() {
|
|
let {content} = this.mm;
|
|
|
|
if (!content) {
|
|
this.mm.removeEventListener("pagehide", this);
|
|
return;
|
|
}
|
|
|
|
if (content.document.documentURI.startsWith("about:tabcrashed")) {
|
|
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
|
|
// Sanity check - we'd better be loading this in a non-remote browser.
|
|
throw new Error("We seem to be navigating away from about:tabcrashed in " +
|
|
"a non-remote browser. This should really never happen.");
|
|
}
|
|
|
|
this.mm.removeEventListener("pagehide", this);
|
|
|
|
// Notify the parent.
|
|
this.mm.sendAsyncMessage("SessionStore:crashedTabRevived");
|
|
}
|
|
}
|
|
}
|
|
|