Bug 1507287 - Make sessionRestore work with session history living in the parent process. r=peterv,mikedeboer

Differential Revision: https://phabricator.services.mozilla.com/D46281

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Alphan Chen 2020-03-19 14:31:52 +00:00
Родитель 93ec4f0381
Коммит cb65e20cb3
22 изменённых файлов: 1062 добавлений и 313 удалений

Просмотреть файл

@ -37,7 +37,6 @@ const whitelist = {
// Session store
"resource:///modules/sessionstore/ContentSessionStore.jsm",
"resource://gre/modules/sessionstore/SessionHistory.jsm",
// Browser front-end
"resource:///actors/AboutReaderChild.jsm",
@ -86,6 +85,10 @@ const intermittently_loaded_whitelist = {
"resource://gre/modules/nsAsyncShutdown.jsm",
"resource://gre/modules/sessionstore/Utils.jsm",
// Session store.
"resource://gre/modules/sessionstore/SessionHistory.jsm",
"resource:///modules/sessionstore/SessionHistoryListener.jsm",
"resource://specialpowers/SpecialPowersChild.jsm",
"resource://specialpowers/WrapPrivileged.jsm",

Просмотреть файл

@ -58,6 +58,8 @@ function ContentRestore(chromeGlobal) {
let EXPORTED_METHODS = [
"restoreHistory",
"finishRestoreHistory",
"restoreOnNewEntry",
"restoreTabContent",
"restoreDocument",
"resetRestore",
@ -94,6 +96,25 @@ function ContentRestoreInternal(chromeGlobal) {
// data from the network. Set in restoreHistory() and restoreTabContent(),
// removed in resetRestore().
this._progressListener = null;
this._shistoryInParent = false;
}
function kickOffNewLoadFromBlankPage(webNavigation, newURI) {
// Reset the tab's URL to what it's actually showing. Without this loadURI()
// would use the current document and change the displayed URL only.
webNavigation.setCurrentURI(Services.io.newURI("about:blank"));
// Kick off a new load so that we navigate away from about:blank to the
// new URL that was passed to loadURI(). The new load will cause a
// STATE_START notification to be sent and the ProgressListener will then
// notify the parent and do the rest.
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
let loadURIOptions = {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
loadFlags,
};
webNavigation.loadURI(newURI, loadURIOptions);
}
/**
@ -111,7 +132,7 @@ ContentRestoreInternal.prototype = {
* non-zero) is passed through to all the callbacks. If a load in the tab
* is started while it is pending, the appropriate callbacks are called.
*/
restoreHistory(tabData, loadArguments, callbacks) {
restoreHistory(tabData, loadArguments, callbacks, shistoryInParent) {
this._tabData = tabData;
// In case about:blank isn't done yet.
@ -130,27 +151,46 @@ ContentRestoreInternal.prototype = {
webNavigation.setCurrentURI(Services.io.newURI(uri));
}
SessionHistory.restore(this.docShell, tabData);
this._shistoryInParent = shistoryInParent;
// Add a listener to watch for reloads.
let listener = new HistoryListener(this.docShell, () => {
// On reload, restore tab contents.
this.restoreTabContent(null, false, callbacks.onLoadFinished);
});
if (this._shistoryInParent) {
callbacks.requestRestoreSHistory();
} else {
SessionHistory.restore(this.docShell, tabData);
webNavigation.sessionHistory.legacySHistory.addSHistoryListener(listener);
this._historyListener = listener;
// Add a listener to watch for reloads.
let listener = new HistoryListener(this.docShell, () => {
// On reload, restore tab contents.
this.restoreTabContent(
null,
false,
callbacks.onLoadFinished,
null,
null
);
});
webNavigation.sessionHistory.legacySHistory.addSHistoryListener(listener);
this._historyListener = listener;
this.finishRestoreHistory(callbacks);
}
},
finishRestoreHistory(callbacks) {
// Make sure to reset the capabilities and attributes in case this tab gets
// reused.
SessionStoreUtils.restoreDocShellCapabilities(
this.docShell,
tabData.disallow
this._tabData.disallow
);
if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) {
SessionStoreUtils.restoreSessionStorage(this.docShell, tabData.storage);
delete tabData.storage;
if (this._tabData.storage && this.docShell instanceof Ci.nsIDocShell) {
SessionStoreUtils.restoreSessionStorage(
this.docShell,
this._tabData.storage
);
delete this._tabData.storage;
}
// Add a progress listener to correctly handle browser.loadURI()
@ -162,7 +202,10 @@ ContentRestoreInternal.prototype = {
this._tabData = null;
// Listen for the tab to finish loading.
this.restoreTabContentStarted(callbacks.onLoadFinished);
this.restoreTabContentStarted(
callbacks.onLoadFinished,
callbacks.removeRestoreListener
);
// Notify the parent.
callbacks.onLoadStarted();
@ -170,11 +213,22 @@ ContentRestoreInternal.prototype = {
});
},
restoreOnNewEntry(newURI) {
let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
kickOffNewLoadFromBlankPage(webNavigation, newURI);
},
/**
* Start loading the current page. When the data has finished loading from the
* network, finishCallback is called. Returns true if the load was successful.
*/
restoreTabContent(loadArguments, isRemotenessUpdate, finishCallback) {
restoreTabContent(
loadArguments,
isRemotenessUpdate,
finishCallback,
removeListenerCallback,
reloadSHistoryCallback
) {
let tabData = this._tabData;
this._tabData = null;
@ -182,7 +236,7 @@ ContentRestoreInternal.prototype = {
let history = webNavigation.sessionHistory.legacySHistory;
// Listen for the tab to finish loading.
this.restoreTabContentStarted(finishCallback);
this.restoreTabContentStarted(finishCallback, removeListenerCallback);
// Reset the current URI to about:blank. We changed it above for
// switch-to-tab, but now it must go back to the correct value before the
@ -273,7 +327,11 @@ ContentRestoreInternal.prototype = {
// In order to work around certain issues in session history, we need to
// force session history to update its internal index and call reload
// instead of gotoIndex. See bug 597315.
history.reloadCurrentEntry();
if (this._shistoryInParent) {
reloadSHistoryCallback();
} else {
history.reloadCurrentEntry();
}
} else {
// If there's nothing to restore, we should still blank the page.
let loadURIOptions = {
@ -298,10 +356,14 @@ ContentRestoreInternal.prototype = {
* To be called after restoreHistory(). Removes all listeners needed for
* pending tabs and makes sure to notify when the tab finished loading.
*/
restoreTabContentStarted(finishCallback) {
restoreTabContentStarted(finishCallback, removeListenerCallback) {
// The reload listener is no longer needed.
this._historyListener.uninstall();
this._historyListener = null;
if (!this._shistoryInParent) {
this._historyListener.uninstall();
this._historyListener = null;
} else {
removeListenerCallback();
}
// Remove the old progress listener.
this._progressListener.uninstall();
@ -415,20 +477,7 @@ HistoryListener.prototype = {
return;
}
// Reset the tab's URL to what it's actually showing. Without this loadURI()
// would use the current document and change the displayed URL only.
this.webNavigation.setCurrentURI(Services.io.newURI("about:blank"));
// Kick off a new load so that we navigate away from about:blank to the
// new URL that was passed to loadURI(). The new load will cause a
// STATE_START notification to be sent and the ProgressListener will then
// notify the parent and do the rest.
let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
let loadURIOptions = {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
loadFlags,
};
this.webNavigation.loadURI(newURI.spec, loadURIOptions);
kickOffNewLoadFromBlankPage(this.webNavigation, newURI);
},
OnHistoryReload() {

Просмотреть файл

@ -21,8 +21,8 @@ ChromeUtils.defineModuleGetter(
);
ChromeUtils.defineModuleGetter(
this,
"SessionHistory",
"resource://gre/modules/sessionstore/SessionHistory.jsm"
"SessionHistoryListener",
"resource:///modules/sessionstore/SessionHistoryListener.jsm"
);
// This pref controls whether or not we send updates to the parent on a timeout
@ -31,9 +31,6 @@ 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;
class Handler {
constructor(store) {
this.store = store;
@ -54,83 +51,8 @@ class Handler {
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.
@ -177,139 +99,6 @@ class EventListener extends Handler {
}
}
/**
* 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);
}
OnHistoryGotoIndex() {
// We ought to collect the previously current entry as well, see bug 1350567.
this.collectFrom(kLastIndex);
}
OnHistoryPurge() {
this.collect();
}
OnHistoryReload() {
this.collect();
return true;
}
OnHistoryReplaceEntry() {
this.collect();
}
}
SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([
Ci.nsISHistoryListener,
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
@ -550,6 +339,9 @@ class MessageQueue extends Handler {
*/
const MESSAGES = [
"SessionStore:restoreHistory",
"SessionStore:finishRestoreHistory",
"SessionStore:OnHistoryReload",
"SessionStore:OnHistoryNewEntry",
"SessionStore:restoreTabContent",
"SessionStore:resetRestore",
"SessionStore:flush",
@ -560,23 +352,30 @@ class ContentSessionStore {
constructor(mm) {
this.mm = mm;
this.messageQueue = new MessageQueue(this);
this.stateChangeNotifier = new StateChangeNotifier(this);
this.epoch = 0;
this.contentRestoreInitialized = false;
this.waitRestoreSHistoryInParent = false;
this.restoreTabContentData = null;
XPCOMUtils.defineLazyGetter(this, "contentRestore", () => {
this.contentRestoreInitialized = true;
return new ContentRestore(mm);
});
this.handlers = [
new EventListener(this),
new SessionHistoryListener(this),
this.stateChangeNotifier,
this.messageQueue,
];
this.handlers = [new EventListener(this), this.messageQueue];
this._shistoryInParent = Services.prefs.getBoolPref(
"fission.sessionHistoryInParent",
false
);
if (this._shistoryInParent) {
this.mm.sendAsyncMessage("SessionStore:addSHistoryListener");
} else {
this.handlers.push(new SessionHistoryListener(this));
}
MESSAGES.forEach(m => mm.addMessageListener(m, this));
@ -597,7 +396,7 @@ class ContentSessionStore {
// 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) {
if (data && data.epoch && data.epoch != this.epoch) {
this.epoch = data.epoch;
}
@ -605,8 +404,45 @@ class ContentSessionStore {
case "SessionStore:restoreHistory":
this.restoreHistory(data);
break;
case "SessionStore:finishRestoreHistory":
this.finishRestoreHistory();
break;
case "SessionStore:OnHistoryNewEntry":
this.contentRestore.restoreOnNewEntry(data.uri);
break;
case "SessionStore:OnHistoryReload":
// On reload, restore tab contents.
this.contentRestore.restoreTabContent(
null,
false,
() => {
// 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: this.epoch,
});
},
() => {
// Tell SessionStore.jsm to remove restoreListener.
this.mm.sendAsyncMessage("SessionStore:removeRestoreListener", {
epoch: this.epoch,
});
},
() => {
// Tell SessionStore.jsm to reload currentEntry.
this.mm.sendAsyncMessage("SessionStore:reloadCurrentEntry", {
epoch: this.epoch,
});
}
);
break;
case "SessionStore:restoreTabContent":
this.restoreTabContent(data);
if (this.waitRestoreSHistoryInParent) {
// Queue the TabContentData if we haven't finished sHistoryRestore yet.
this.restoreTabContentData = data;
} else {
this.restoreTabContent(data);
}
break;
case "SessionStore:resetRestore":
this.contentRestore.resetRestore();
@ -615,7 +451,9 @@ class ContentSessionStore {
this.flush(data);
break;
case "SessionStore:becomeActiveProcess":
SessionHistoryListener.collect();
if (!this._shistoryInParent) {
SessionHistoryListener.collect();
}
break;
default:
debug("received unknown message '" + name + "'");
@ -624,27 +462,55 @@ class ContentSessionStore {
}
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.
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,
});
},
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,
});
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,
});
},
removeRestoreListener: () => {
if (!this._shistoryInParent) {
return;
}
// Notify the parent that the tab is no longer pending.
this.mm.sendAsyncMessage("SessionStore:removeRestoreListener", {
epoch,
});
},
requestRestoreSHistory: () => {
if (!this._shistoryInParent) {
return;
}
this.waitRestoreSHistoryInParent = true;
// Send tabData to the parent process.
this.mm.sendAsyncMessage("SessionStore:restoreSHistoryInParent", {
epoch,
});
},
},
});
this._shistoryInParent
);
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
// For non-remote tabs, when restoreHistory finishes, we send a synchronous
@ -661,7 +527,7 @@ class ContentSessionStore {
epoch,
isRemotenessUpdate,
});
} else {
} else if (!this._shistoryInParent) {
this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", {
epoch,
isRemotenessUpdate,
@ -669,6 +535,49 @@ class ContentSessionStore {
}
}
finishRestoreHistory() {
this.contentRestore.finishRestoreHistory({
// 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: this.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: this.epoch,
});
},
removeRestoreListener: () => {
if (!this._shistoryInParent) {
return;
}
// Notify the parent that the tab is no longer pending.
this.mm.sendAsyncMessage("SessionStore:removeRestoreListener", {
epoch: this.epoch,
});
},
});
this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", {
epoch: this.epoch,
});
if (this.restoreTabContentData) {
this.restoreTabContent(this.restoreTabContentData);
this.restoreTabContentData = null;
}
this.waitRestoreSHistoryInParent = false;
}
restoreTabContent({ loadArguments, isRemotenessUpdate, reason }) {
let epoch = this.epoch;
@ -683,6 +592,17 @@ class ContentSessionStore {
epoch,
isRemotenessUpdate,
});
},
() => {
// Tell SessionStore.jsm to remove restore listener.
this.mm.sendAsyncMessage("SessionStore:removeRestoreListener", {
epoch,
});
},
() => {
this.mm.sendAsyncMessage("SessionStore:reloadCurrentEntry", {
epoch,
});
}
);

Просмотреть файл

@ -0,0 +1,232 @@
/* 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 = ["SessionHistoryListener"];
ChromeUtils.defineModuleGetter(
this,
"SessionHistory",
"resource://gre/modules/sessionstore/SessionHistory.jsm"
);
const kNoIndex = Number.MAX_SAFE_INTEGER;
const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
/**
* 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 {
constructor(store) {
this.store = store;
this._observers = new Set();
let webProgress = this.mm.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(
this,
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
);
}
get mm() {
return this.store.mm;
}
/**
* 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 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 {
constructor(store) {
this.store = 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 = new StateChangeNotifier(this);
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.store.messageQueue.send();
}
// Listen for page title changes.
this.mm.addEventListener("DOMTitleChanged", this);
}
get mm() {
return this.store.mm;
}
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.store.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);
}
OnHistoryGotoIndex() {
// We ought to collect the previously current entry as well, see bug 1350567.
this.collectFrom(kLastIndex);
}
OnHistoryPurge() {
this.collect();
}
OnHistoryReload() {
this.collect();
return true;
}
OnHistoryReplaceEntry() {
this.collect();
}
}
SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([
Ci.nsISHistoryListener,
Ci.nsISupportsWeakReference,
]);

Просмотреть файл

@ -97,6 +97,22 @@ const MESSAGES = [
// The content script encountered an error.
"SessionStore:error",
// The content script asks us to add the session history listener in the
// parent process when sessionHistory is in the parent process.
"SessionStore:addSHistoryListener",
// The content script asks us to remove the session history listener which
// is added in the restore process when sessionHistory is in the parent process.
"SessionStore:removeRestoreListener",
// The content script asks us to restore session history in the parent process
// when sessionHistory is in the parent process.
"SessionStore:restoreSHistoryInParent",
// The content script asks us to reload the current session history entry when
// sessionHistory is in the parent process.
"SessionStore:reloadCurrentEntry",
];
// The list of messages we accept from <xul:browser>s that have no tab
@ -112,6 +128,9 @@ const NOTAB_MESSAGES = new Set([
// For a description see above.
"SessionStore:error",
// For a description see above.
"SessionStore:addSHistoryListener",
]);
// The list of messages we accept without an "epoch" parameter.
@ -122,6 +141,9 @@ const NOEPOCH_MESSAGES = new Set([
// For a description see above.
"SessionStore:error",
// For a description see above.
"SessionStore:addSHistoryListener",
]);
// The list of messages we want to receive even during the short period after a
@ -175,6 +197,10 @@ const RESTORE_TAB_CONTENT_REASON = {
// 'browser.startup.page' preference value to resume the previous session.
const BROWSER_STARTUP_RESUME_SESSION = 3;
// Used by SessionHistoryListener.
const kNoIndex = Number.MAX_SAFE_INTEGER;
const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
ChromeUtils.import("resource://gre/modules/TelemetryTimestamps.jsm", this);
@ -182,6 +208,12 @@ ChromeUtils.import("resource://gre/modules/Timer.jsm", this);
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
ChromeUtils.import("resource://gre/modules/osfile.jsm", this);
ChromeUtils.defineModuleGetter(
this,
"SessionHistory",
"resource://gre/modules/sessionstore/SessionHistory.jsm"
);
XPCOMUtils.defineLazyServiceGetters(this, {
gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"],
Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"],
@ -501,6 +533,15 @@ var SessionStoreInternal = {
// windows yet to be restored
_restoreCount: -1,
// For each <browser> element, records the SHistoryListener.
_browserSHistoryListener: new WeakMap(),
// For each <browser> element, records the SHistoryListener.
_browserSHistoryListenerForRestore: new WeakMap(),
// The history data needed to be restored in the parent.
_shistoryToRestore: new WeakMap(),
// For each <browser> element, records the current epoch.
_browserEpochs: new WeakMap(),
@ -824,6 +865,11 @@ var SessionStoreInternal = {
"privacy.resistFingerprinting"
);
Services.prefs.addObserver("privacy.resistFingerprinting", this);
this._shistoryInParent = Services.prefs.getBoolPref(
"fission.sessionHistoryInParent",
false
);
},
/**
@ -907,6 +953,205 @@ var SessionStoreInternal = {
}
},
// Create a sHistoryLister and register it.
// We also need to save the SHistoryLister into this._browserSHistoryListener.
addSHistoryListener(aBrowser) {
function SHistoryListener(browser) {
browser.frameLoader.browsingContext.sessionHistory.addSHistoryListener(
this
);
this.browser = browser;
this.frameLoader = browser.frameLoader;
this._fromIdx = kNoIndex;
this._sHistoryChanges = false;
if (this.browser.currentURI && this.browser.ownerGlobal) {
this._lastKnownUri = browser.currentURI.displaySpec;
this._lastKnownBody = browser.ownerGlobal.document.body;
this._lastKnownUserContextId =
browser.contentPrincipal.originAttributes.userContextId;
}
}
SHistoryListener.prototype = {
QueryInterface: ChromeUtils.generateQI([
Ci.nsISHistoryListener,
Ci.nsISupportsWeakReference,
]),
notifySHistoryChanges(index) {
if (this._fromIdx <= index) {
// If we already know that we need to update history from index N we can ignore any changes
// that 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;
}
if (!this._sHistoryChanges) {
this.frameLoader.requestSHistoryUpdate(/*aImmediately*/ false);
this._sHistoryChanges = true;
}
this._fromIdx = index;
if (this.browser.currentURI && this.browser.ownerGlobal) {
this._lastKnownUri = this.browser.currentURI.displaySpec;
this._lastKnownBody = this.browser.ownerGlobal.document.body;
this._lastKnownUserContextId = this.browser.contentPrincipal.originAttributes.userContextId;
}
},
uninstall() {
if (this.frameLoader.browsingContext) {
let shistory = this.frameLoader.browsingContext.sessionHistory;
if (shistory) {
shistory.removeSHistoryListener(this);
}
}
},
OnHistoryNewEntry(newURI, oldIndex) {
this.notifySHistoryChanges(oldIndex);
},
OnHistoryGotoIndex() {
this.notifySHistoryChanges(kLastIndex);
},
OnHistoryPurge() {
this.notifySHistoryChanges(-1);
},
OnHistoryReload() {
this.notifySHistoryChanges(-1);
return true;
},
OnHistoryReplaceEntry() {
this.notifySHistoryChanges(-1);
let win = this.browser.ownerGlobal;
let tab = win ? win.gBrowser.getTabForBrowser(this.browser) : null;
if (tab) {
let event = tab.ownerDocument.createEvent("CustomEvent");
event.initCustomEvent("SSHistoryReplaceEntry", true, false);
tab.dispatchEvent(event);
}
},
};
let spec = null;
if (aBrowser.currentURI) {
spec = aBrowser.currentURI.displaySpec;
}
if (!aBrowser.frameLoader) {
debug(
"addSHistoryListener(), aBrowser.frameLoader doesn't exist" +
",browser.currentURI.displaySpec=" +
spec
);
return;
}
if (!aBrowser.frameLoader.browsingContext) {
debug(
"addSHistoryListener(), aBrowser.fl.browsingContext doesn't exists" +
",browser.currentURI.displaySpec=" +
spec
);
return;
}
if (!aBrowser.frameLoader.browsingContext.sessionHistory) {
debug(
"addSHistoryListener(), aBrowser.fl.bc.sessionHistory doesn't exists" +
",browser.currentURI.displaySpec=" +
spec
);
return;
}
let listener = new SHistoryListener(aBrowser);
this._browserSHistoryListener.set(aBrowser.permanentKey, listener);
// Collect data if we start with a non-empty shistory.
let uri = aBrowser.currentURI.displaySpec;
let history = aBrowser.frameLoader.browsingContext.sessionHistory;
if (uri != "about:blank" || history.count != 0) {
aBrowser.frameLoader.requestSHistoryUpdate(/*aImmediately*/ true);
}
},
/**
* This listener detects when a page being restored is reloaded. It triggers a
* callback and cancels the reload. The callback will send a message to
* SessionStore.jsm so that it can restore the content immediately.
*/
addSHistoryListenerForRestore(aBrowser) {
function SHistoryListener(browser) {
browser.frameLoader.browsingContext.sessionHistory.addSHistoryListener(
this
);
this.browser = browser;
}
SHistoryListener.prototype = {
QueryInterface: ChromeUtils.generateQI([
Ci.nsISHistoryListener,
Ci.nsISupportsWeakReference,
]),
uninstall() {
let shistory = this.browser.frameLoader.browsingContext.sessionHistory;
if (shistory) {
shistory.removeSHistoryListener(this);
}
},
OnHistoryGotoIndex() {},
OnHistoryPurge() {},
OnHistoryReplaceEntry() {},
// This will be called for a pending tab when loadURI(uri) is called where
// the given |uri| only differs in the fragment.
OnHistoryNewEntry(newURI) {
let currentURI = this.browser.currentURI;
// Ignore new SHistory entries with the same URI as those do not indicate
// a navigation inside a document by changing the #hash part of the URL.
// We usually hit this when purging session history for browsers.
if (currentURI && currentURI.displaySpec == newURI.spec) {
return;
}
// Notify ContentSessionStore.jsm to restore on new entry.
this.browser.messageManager.sendAsyncMessage(
"SessionStore:OnHistoryNewEntry",
{ uri: newURI.spec }
);
},
OnHistoryReload() {
// Notify ContentSessionStore.jsm to restore tab contents.
this.browser.messageManager.sendAsyncMessage(
"SessionStore:OnHistoryReload"
);
// Cancel the load.
return false;
},
};
if (
!aBrowser.frameLoader ||
!aBrowser.frameLoader.browsingContext ||
!aBrowser.frameLoader.browsingContext.sessionHistory
) {
return;
}
let listener = new SHistoryListener(aBrowser);
this._browserSHistoryListenerForRestore.set(
aBrowser.permanentKey,
listener
);
},
updateSessionStoreFromTablistener(aBrowser, aData) {
if (aBrowser.permanentKey == undefined) {
return;
@ -917,6 +1162,72 @@ var SessionStoreInternal = {
return;
}
let sHistoryChangedInListener = false;
let listener = this._browserSHistoryListener.get(aBrowser.permanentKey);
if (listener) {
sHistoryChangedInListener = listener._sHistoryChanges;
}
if (aData.sHistoryNeeded || sHistoryChangedInListener) {
if (!listener) {
debug(
"updateSessionStoreFromTablistener() with aData.sHistoryNeeded, but no SHlistener. Add again!!!"
);
this.addSHistoryListener(aBrowser);
listener = this._browserSHistoryListener.get(aBrowser.permanentKey);
}
if (listener) {
if (!aData.sHistoryNeeded && listener._fromIdx == kNoIndex) {
// No shistory changes needed.
listener._sHistoryChanges = false;
} else {
// |browser.frameLoader| might be empty if the browser was already
// destroyed and its tab removed. In that case we still have the last
// frameLoader we know about to compare.
let frameLoader =
aBrowser.frameLoader ||
this._lastKnownFrameLoader.get(aBrowser.permanentKey);
if (
frameLoader &&
frameLoader.browsingContext &&
frameLoader.browsingContext.sessionHistory
) {
let uri = aBrowser.currentURI
? aBrowser.currentURI.displaySpec
: listener._lastKnownUri;
let body = aBrowser.ownerGlobal
? aBrowser.ownerGlobal.document.body
: listener._lastKnownBody;
let userContextId = aBrowser.contentPrincipal
? aBrowser.contentPrincipal.originAttributes.userContextId
: listener._lastKnownUserContextId;
aData.data.historychange = SessionHistory.collectFromParent(
uri,
body,
frameLoader.browsingContext.sessionHistory,
userContextId,
listener._sHistoryChanges ? listener._fromIdx : -1
);
listener._sHistoryChanges = false;
listener._fromIdx = kNoIndex;
} else {
debug(
"updateSessionStoreFromTablistener() with sHistoryNeeded, but no fL.bC.sessionHistory."
);
}
}
} else {
debug(
"updateSessionStoreFromTablistener() with sHistoryNeeded, but no sHlistener."
);
}
}
if ("sHistoryNeeded" in aData) {
delete aData.sHistoryNeeded;
}
TabState.update(aBrowser, aData);
let win = aBrowser.ownerGlobal;
this.saveStateDelayed(win);
@ -965,6 +1276,34 @@ var SessionStoreInternal = {
}
switch (aMessage.name) {
case "SessionStore:addSHistoryListener":
this.addSHistoryListener(browser);
break;
case "SessionStore:restoreSHistoryInParent":
if (
browser.frameLoader &&
browser.frameLoader.browsingContext &&
browser.frameLoader.browsingContext.sessionHistory
) {
let tabData = this._shistoryToRestore.get(browser.permanentKey);
if (tabData) {
this._shistoryToRestore.delete(browser.permanentKey);
SessionHistory.restoreFromParent(
browser.frameLoader.browsingContext.sessionHistory,
tabData
);
}
this.addSHistoryListenerForRestore(browser);
} else {
debug(
"receive SessionStore:restoreSHistoryInParent: but cannot find sessionHistory from bc."
);
}
browser.messageManager.sendAsyncMessage(
"SessionStore:finishRestoreHistory"
);
break;
case "SessionStore:update":
// |browser.frameLoader| might be empty if the browser was already
// destroyed and its tab removed. In that case we still have the last
@ -984,6 +1323,13 @@ var SessionStoreInternal = {
// late and will never respond. If they have been sent shortly after
// switching a browser's remoteness there isn't too much data to skip.
TabStateFlusher.resolveAll(browser);
let listener = this._browserSHistoryListener.get(
browser.permanentKey
);
if (listener) {
listener.uninstall();
this._browserSHistoryListener.delete(browser.permanentKey);
}
} else if (aMessage.data.flushID) {
// This is an update kicked off by an async flush request. Notify the
// TabStateFlusher so that it can finish the request and notify its
@ -1073,6 +1419,39 @@ var SessionStoreInternal = {
tab.dispatchEvent(event);
break;
}
case "SessionStore:removeRestoreListener":
let listener = this._browserSHistoryListenerForRestore.get(
browser.permanentKey
);
if (listener) {
listener.uninstall();
this._browserSHistoryListenerForRestore.delete(browser.permanentKey);
}
break;
case "SessionStore:reloadCurrentEntry":
let fL =
browser.frameLoader ||
this._lastKnownFrameLoader.get(browser.permanentKey);
if (fL) {
if (fL.browsingContext) {
if (fL.browsingContext.sessionHistory) {
fL.browsingContext.sessionHistory.reloadCurrentEntry();
} else {
debug(
"receive SessionStore:reloadCurrentEntry browser.fL.bC.sessionHistory is null."
);
}
} else {
debug(
"receive SessionStore:reloadCurrentEntry browser.fL.browsingContext is null."
);
}
} else {
debug(
"receive SessionStore:reloadCurrentEntry browser.frameLoader is null."
);
}
break;
case "SessionStore:restoreTabContentStarted":
if (TAB_STATE_FOR_BROWSER.get(browser) == TAB_STATE_NEEDS_RESTORE) {
// If a load not initiated by sessionstore was started in a
@ -1213,6 +1592,11 @@ var SessionStoreInternal = {
epoch: newEpoch,
}
);
let listener = this._browserSHistoryListener.get(target.permanentKey);
if (listener) {
listener.notifySHistoryChanges(-1);
}
break;
default:
throw new Error(`unhandled event ${aEvent.type}?`);
@ -5799,6 +6183,11 @@ var SessionStoreInternal = {
}
}
if (this._shistoryInParent) {
// Save the history data for restoring in the parent process.
this._shistoryToRestore.set(browser.permanentKey, options.tabData);
}
browser.messageManager.sendAsyncMessage(
"SessionStore:restoreHistory",
options

Просмотреть файл

@ -17,6 +17,7 @@ EXTRA_JS_MODULES.sessionstore = [
'RunState.jsm',
'SessionCookies.jsm',
'SessionFile.jsm',
'SessionHistoryListener.jsm',
'SessionMigration.jsm',
'SessionSaver.jsm',
'SessionStartup.jsm',

Просмотреть файл

@ -32,12 +32,26 @@ function restoreClosedTabWithValue(rval) {
return ss.undoCloseTab(window, index);
}
function promiseNewLocationAndHistoryEntryReplaced(browser, snippet) {
function promiseNewLocationAndHistoryEntryReplaced(tab, snippet) {
let browser = tab.linkedBrowser;
if (Services.prefs.getBoolPref("fission.sessionHistoryInParent", false)) {
SpecialPowers.spawn(browser, [snippet], async function(codeSnippet) {
// Need to define 'webNavigation' for 'codeSnippet'
// eslint-disable-next-line no-unused-vars
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
// Evaluate the snippet that changes the location.
// eslint-disable-next-line no-eval
eval(codeSnippet);
});
return promiseOnHistoryReplaceEntry(tab);
}
return SpecialPowers.spawn(browser, [snippet], async function(codeSnippet) {
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let shistory = webNavigation.sessionHistory.legacySHistory;
// Evaluate the snippet that the changes the location.
// Evaluate the snippet that changes the location.
// eslint-disable-next-line no-eval
eval(codeSnippet);
@ -121,7 +135,7 @@ add_task(async function save_worthy_tabs_remote_final() {
let snippet =
'webNavigation.loadURI("https://example.com/",\
{triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()})';
await promiseNewLocationAndHistoryEntryReplaced(browser, snippet);
await promiseNewLocationAndHistoryEntryReplaced(tab, snippet);
// Remotness shouldn't have changed.
ok(browser.isRemoteBrowser, "browser is still remote");
@ -164,11 +178,10 @@ add_task(async function save_worthy_tabs_nonremote_final() {
add_task(async function dont_save_empty_tabs_final() {
let { tab, r } = await createTabWithRandomValue("https://example.com/");
let browser = tab.linkedBrowser;
// Replace the current page with an about:blank entry.
let snippet = 'content.location.replace("about:blank")';
await promiseNewLocationAndHistoryEntryReplaced(browser, snippet);
await promiseNewLocationAndHistoryEntryReplaced(tab, snippet);
// Remove the tab before the update arrives.
let promise = promiseRemoveTabAndSessionState(tab);

Просмотреть файл

@ -38,8 +38,12 @@ add_task(async function test_add_interesting_window() {
content.location = newPage;
});
await promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry");
if (Services.prefs.getBoolPref("fission.sessionHistoryInParent", false)) {
let tab = newWin.gBrowser.selectedTab;
await promiseOnHistoryReplaceEntry(tab);
} else {
await promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry");
}
// Clear out the userTypedValue so that the new window looks like
// it's really not worth restoring.
browser.userTypedValue = null;

Просмотреть файл

@ -556,6 +556,10 @@ function promiseDelayedStartupFinished(aWindow) {
return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve));
}
function promiseOnHistoryReplaceEntry(tab) {
return BrowserTestUtils.waitForEvent(tab, "SSHistoryReplaceEntry");
}
function promiseTabRestored(tab) {
return BrowserTestUtils.waitForEvent(tab, "SSTabRestored");
}

Просмотреть файл

@ -3209,6 +3209,18 @@ void nsFrameLoader::RequestEpochUpdate(uint32_t aEpoch) {
}
}
void nsFrameLoader::RequestSHistoryUpdate(bool aImmediately) {
if (mSessionStoreListener) {
mSessionStoreListener->UpdateSHistoryChanges(aImmediately);
return;
}
// If remote browsing (e10s), handle this with the BrowserParent.
if (auto* browserParent = GetBrowserParent()) {
Unused << browserParent->SendUpdateSHistory(aImmediately);
}
}
void nsFrameLoader::Print(uint64_t aOuterWindowID,
nsIPrintSettings* aPrintSettings,
nsIWebProgressListener* aProgressListener,

Просмотреть файл

@ -211,6 +211,8 @@ class nsFrameLoader final : public nsStubMutationObserver,
void RequestEpochUpdate(uint32_t aEpoch);
void RequestSHistoryUpdate(bool aImmediately = false);
void Print(uint64_t aOuterWindowID, nsIPrintSettings* aPrintSettings,
nsIWebProgressListener* aProgressListener,
mozilla::ErrorResult& aRv);

Просмотреть файл

@ -2015,6 +2015,14 @@ mozilla::ipc::IPCResult BrowserChild::RecvUpdateEpoch(const uint32_t& aEpoch) {
return IPC_OK();
}
mozilla::ipc::IPCResult BrowserChild::RecvUpdateSHistory(
const bool& aImmediately) {
if (mSessionStoreListener) {
mSessionStoreListener->UpdateSHistoryChanges(aImmediately);
}
return IPC_OK();
}
// In case handling repeated keys takes much time, we skip firing new ones.
bool BrowserChild::SkipRepeatedKeyEvent(const WidgetKeyboardEvent& aEvent) {
if (mRepeatedKeyEventTime.IsNull() || !aEvent.CanSkipInRemoteProcess() ||
@ -4003,8 +4011,9 @@ bool BrowserChild::UpdateSessionStore(uint32_t aFlushId, bool aIsFinal) {
Unused << SendSessionStoreUpdate(
docShellCaps, privatedMode, positions, positionDescendants, inputs,
idVals, xPathVals, origins, keys, values, isFullStorage, aFlushId,
aIsFinal, mSessionStoreListener->GetEpoch());
idVals, xPathVals, origins, keys, values, isFullStorage,
store->GetAndClearSHistoryChanged(), aFlushId, aIsFinal,
mSessionStoreListener->GetEpoch());
return true;
}

Просмотреть файл

@ -381,6 +381,8 @@ class BrowserChild final : public nsMessageManagerScriptExecutor,
mozilla::ipc::IPCResult RecvUpdateEpoch(const uint32_t& aEpoch);
mozilla::ipc::IPCResult RecvUpdateSHistory(const bool& aImmediately);
mozilla::ipc::IPCResult RecvNativeSynthesisResponse(
const uint64_t& aObserverId, const nsCString& aResponse);

Просмотреть файл

@ -2925,7 +2925,8 @@ mozilla::ipc::IPCResult BrowserParent::RecvSessionStoreUpdate(
const nsTArray<CollectedInputDataValue>& aXPathVals,
nsTArray<nsCString>&& aOrigins, nsTArray<nsString>&& aKeys,
nsTArray<nsString>&& aValues, const bool aIsFullStorage,
const uint32_t& aFlushId, const bool& aIsFinal, const uint32_t& aEpoch) {
const bool aNeedCollectSHistory, const uint32_t& aFlushId,
const bool& aIsFinal, const uint32_t& aEpoch) {
UpdateSessionStoreData data;
if (aDocShellCaps.isSome()) {
data.mDocShellCaps.Construct() = aDocShellCaps.value();
@ -2982,8 +2983,8 @@ mozilla::ipc::IPCResult BrowserParent::RecvSessionStoreUpdate(
bool ok = ToJSValue(jsapi.cx(), data, &dataVal);
NS_ENSURE_TRUE(ok, IPC_OK());
nsresult rv = funcs->UpdateSessionStore(mFrameElement, aFlushId, aIsFinal,
aEpoch, dataVal);
nsresult rv = funcs->UpdateSessionStore(
mFrameElement, aFlushId, aIsFinal, aEpoch, dataVal, aNeedCollectSHistory);
NS_ENSURE_SUCCESS(rv, IPC_OK());
return IPC_OK();

Просмотреть файл

@ -340,7 +340,8 @@ class BrowserParent final : public PBrowserParent,
const nsTArray<CollectedInputDataValue>& aXPathVals,
nsTArray<nsCString>&& aOrigins, nsTArray<nsString>&& aKeys,
nsTArray<nsString>&& aValues, const bool aIsFullStorage,
const uint32_t& aFlushId, const bool& aIsFinal, const uint32_t& aEpoch);
const bool aNeedCollectSHistory, const uint32_t& aFlushId,
const bool& aIsFinal, const uint32_t& aEpoch);
mozilla::ipc::IPCResult RecvBrowserFrameOpenWindow(
PBrowserParent* aOpener, const nsString& aURL, const nsString& aName,

Просмотреть файл

@ -616,12 +616,14 @@ parent:
CollectedInputDataValue[] aXPathVals,
nsCString[] aOrigins, nsString[] aKeys,
nsString[] aValues, bool aIsFullStorage,
uint32_t aFlushId, bool aIsFinal, uint32_t aEpoch);
bool aNeedCollectSHistory, uint32_t aFlushId,
bool aIsFinal, uint32_t aEpoch);
child:
async NativeSynthesisResponse(uint64_t aObserverId, nsCString aResponse);
async FlushTabState(uint32_t aFlushId, bool aIsFinal);
async UpdateEpoch(uint32_t aEpoch);
async UpdateSHistory(bool aImmediately);
parent:

Просмотреть файл

@ -112,6 +112,11 @@ interface FrameLoader {
*/
void requestEpochUpdate(unsigned long aEpoch);
/**
* Request a session history update in native sessionStoreListeners.
*/
void requestSHistoryUpdate(boolean aImmediately);
/**
* Print the current document.
*

Просмотреть файл

@ -11,5 +11,7 @@ webidl Element;
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 uint32_t aFlushId, in boolean aIsFinal, in uint32_t aEpoch, in jsval aData);
void UpdateSessionStore(
in Element aBrowser, in uint32_t aFlushId, in boolean aIsFinal,
in uint32_t aEpoch, in jsval aData, in boolean aCollectSHistory);
};

Просмотреть файл

@ -8,13 +8,21 @@ XPCOMUtils.defineLazyModuleGetters(this, {
SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
});
function UpdateSessionStore(aBrowser, aFlushId, aIsFinal, aEpoch, aData) {
function UpdateSessionStore(
aBrowser,
aFlushId,
aIsFinal,
aEpoch,
aData,
aCollectSHistory
) {
return SessionStoreFuncInternal.updateSessionStore(
aBrowser,
aFlushId,
aIsFinal,
aEpoch,
aData
aData,
aCollectSHistory
);
}
@ -382,7 +390,8 @@ var SessionStoreFuncInternal = {
aFlushId,
aIsFinal,
aEpoch,
aData
aData,
aCollectSHistory
) {
let currentData = {};
if (aData.docShellCaps != undefined) {
@ -434,6 +443,7 @@ var SessionStoreFuncInternal = {
flushID: aFlushId,
isFinal: aIsFinal,
epoch: aEpoch,
sHistoryNeeded: aCollectSHistory,
});
this._formDataId = [];
this._formDataIdValue = [];

Просмотреть файл

@ -9,6 +9,7 @@
#include "mozilla/dom/SessionStoreUtilsBinding.h"
#include "mozilla/dom/StorageEvent.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/StaticPrefs_fission.h"
#include "nsGenericHTMLElement.h"
#include "nsDocShell.h"
#include "nsIAppWindow.h"
@ -44,7 +45,10 @@ ContentSessionStore::ContentSessionStore(nsIDocShell* aDocShell)
mScrollChanged(NO_CHANGE),
mFormDataChanged(NO_CHANGE),
mStorageStatus(NO_STORAGE),
mDocCapChanged(false) {
mDocCapChanged(false),
mSHistoryInParent(StaticPrefs::fission_sessionHistoryInParent()),
mSHistoryChanged(false),
mSHistoryChangedFromParent(false) {
MOZ_ASSERT(mDocShell);
// Check that value at startup as it might have
// been set before the frame script was loaded.
@ -123,11 +127,19 @@ void ContentSessionStore::OnDocumentStart() {
}
SetFullStorageNeeded();
if (mSHistoryInParent) {
mSHistoryChanged = true;
}
}
void ContentSessionStore::OnDocumentEnd() {
mScrollChanged = WITH_CHANGE;
SetFullStorageNeeded();
if (mSHistoryInParent) {
mSHistoryChanged = true;
}
}
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TabListener)
@ -156,7 +168,8 @@ TabListener::TabListener(nsIDocShell* aDocShell, Element* aElement)
mUpdatedTimer(nullptr),
mTimeoutDisabled(false),
mUpdateInterval(15000),
mEpoch(0) {
mEpoch(0),
mSHistoryInParent(StaticPrefs::fission_sessionHistoryInParent()) {
MOZ_ASSERT(mDocShell);
}
@ -206,6 +219,12 @@ nsresult TabListener::Init() {
eventTarget->AddSystemEventListener(NS_LITERAL_STRING("mozvisualscroll"),
this, false);
eventTarget->AddSystemEventListener(NS_LITERAL_STRING("input"), this, false);
if (mSHistoryInParent) {
eventTarget->AddSystemEventListener(NS_LITERAL_STRING("DOMTitleChanged"),
this, false);
}
mEventListenerRegistered = true;
eventTarget->AddSystemEventListener(
NS_LITERAL_STRING("MozSessionStorageChanged"), this, false);
@ -347,6 +366,9 @@ TabListener::HandleEvent(Event* aEvent) {
if (mSessionStore->AppendSessionStorageChange(event)) {
AddTimerForUpdate();
}
} else if (eventType.EqualsLiteral("DOMTitleChanged")) {
mSessionStore->SetSHistoryChanged();
AddTimerForUpdate();
}
return NS_OK;
@ -641,6 +663,15 @@ bool TabListener::ForceFlushFromParent(uint32_t aFlushId, bool aIsFinal) {
return UpdateSessionStore(aFlushId, aIsFinal);
}
void TabListener::UpdateSHistoryChanges(bool aImmediately) {
mSessionStore->SetSHistoryFromParentChanged();
if (aImmediately) {
UpdateSessionStore();
} else {
AddTimerForUpdate();
}
}
bool TabListener::UpdateSessionStore(uint32_t aFlushId, bool aIsFinal) {
if (!aFlushId) {
if (!mSessionStore || !mSessionStore->UpdateNeeded()) {
@ -740,8 +771,9 @@ bool TabListener::UpdateSessionStore(uint32_t aFlushId, bool aIsFinal) {
bool ok = ToJSValue(jsapi.cx(), data, &dataVal);
NS_ENSURE_TRUE(ok, false);
nsresult rv = funcs->UpdateSessionStore(mOwnerContent, aFlushId, aIsFinal,
mEpoch, dataVal);
nsresult rv = funcs->UpdateSessionStore(
mOwnerContent, aFlushId, aIsFinal, mEpoch, dataVal,
mSessionStore->GetAndClearSHistoryChanged());
NS_ENSURE_SUCCESS(rv, false);
StopTimerForUpdate();
return true;
@ -791,6 +823,10 @@ void TabListener::RemoveListeners() {
NS_LITERAL_STRING("mozvisualscroll"), this, false);
eventTarget->RemoveSystemEventListener(NS_LITERAL_STRING("input"), this,
false);
if (mSHistoryInParent) {
eventTarget->RemoveSystemEventListener(
NS_LITERAL_STRING("DOMTitleChanged"), this, false);
}
mEventListenerRegistered = false;
}
if (mStorageChangeListenerRegistered) {

Просмотреть файл

@ -65,11 +65,24 @@ class ContentSessionStore {
// Return true if there is a new storage change which is appended.
bool AppendSessionStorageChange(StorageEvent* aEvent);
void SetSHistoryChanged() { mSHistoryChanged = mSHistoryInParent; }
// request "collect sessionHistory" which is happened in the parent process
void SetSHistoryFromParentChanged() {
mSHistoryChangedFromParent = mSHistoryInParent;
}
bool GetAndClearSHistoryChanged() {
bool ret = mSHistoryChanged;
mSHistoryChanged = false;
mSHistoryChangedFromParent = false;
return ret;
}
void OnDocumentStart();
void OnDocumentEnd();
bool UpdateNeeded() {
return mPrivateChanged || mDocCapChanged || IsScrollPositionChanged() ||
IsFormDataChanged() || IsStorageUpdated();
IsFormDataChanged() || IsStorageUpdated() || mSHistoryChanged ||
mSHistoryChangedFromParent;
}
private:
@ -97,6 +110,17 @@ class ContentSessionStore {
nsTArray<nsCString> mOrigins;
nsTArray<nsString> mKeys;
nsTArray<nsString> mValues;
// need to collect sessionHistory
bool mSHistoryInParent;
// 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;
// mSHistoryChangedFromParent means there are history changes which
// are found by session history listener in the parent process.
bool mSHistoryChangedFromParent;
};
class TabListener : public nsIDOMEventListener,
@ -114,6 +138,7 @@ class TabListener : public nsIDOMEventListener,
void RemoveListeners();
void SetEpoch(uint32_t aEpoch) { mEpoch = aEpoch; }
uint32_t GetEpoch() { return mEpoch; }
void UpdateSHistoryChanges(bool aImmediately);
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TabListener, nsIDOMEventListener)
@ -145,6 +170,8 @@ class TabListener : public nsIDOMEventListener,
bool mTimeoutDisabled;
int32_t mUpdateInterval;
uint32_t mEpoch;
// sessionHistory in the parent process
bool mSHistoryInParent;
};
} // namespace dom

Просмотреть файл

@ -39,8 +39,26 @@ var SessionHistory = Object.freeze({
return SessionHistoryInternal.collect(docShell, aFromIdx);
},
collectFromParent(uri, body, history, userContextId, aFromIdx = -1) {
return SessionHistoryInternal.collectCommon(
uri,
body,
history,
userContextId,
aFromIdx
);
},
restore(docShell, tabData) {
return SessionHistoryInternal.restore(docShell, tabData);
return SessionHistoryInternal.restore(
docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory
.legacySHistory,
tabData
);
},
restoreFromParent(history, tabData) {
return SessionHistoryInternal.restore(history, tabData);
},
});
@ -81,12 +99,24 @@ var SessionHistoryInternal = {
collect(docShell, aFromIdx = -1) {
let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let uri = webNavigation.currentURI.displaySpec;
let body = webNavigation.document.body;
let history = webNavigation.sessionHistory;
let userContextId = loadContext.originAttributes.userContextId;
return this.collectCommon(
uri,
body,
history.legacySHistory,
userContextId,
aFromIdx
);
},
collectCommon(uri, body, shistory, userContextId, aFromIdx) {
let data = {
entries: [],
userContextId: loadContext.originAttributes.userContextId,
requestedIndex: history.legacySHistory.requestedIndex + 1,
userContextId,
requestedIndex: shistory.requestedIndex + 1,
};
// We want to keep track how many entries we *could* have collected and
@ -95,8 +125,7 @@ var SessionHistoryInternal = {
let skippedCount = 0,
entryCount = 0;
if (history && history.count > 0) {
let shistory = history.legacySHistory.QueryInterface(Ci.nsISHistory);
if (shistory && shistory.count > 0) {
let count = shistory.count;
for (; entryCount < count; entryCount++) {
let shEntry = shistory.getEntryAtIndex(entryCount);
@ -109,15 +138,13 @@ var SessionHistoryInternal = {
}
// Ensure the index isn't out of bounds if an exception was thrown above.
data.index = Math.min(history.index + 1, entryCount);
data.index = Math.min(shistory.index + 1, entryCount);
}
// If either the session history isn't available yet or doesn't have any
// valid entries, make sure we at least include the current page,
// unless of course we just skipped all entries because aFromIdx was big enough.
if (!data.entries.length && (skippedCount != entryCount || aFromIdx < 0)) {
let uri = webNavigation.currentURI.displaySpec;
let body = webNavigation.document.body;
// We landed here because the history is inaccessible or there are no
// history entries. In that case we should at least record the docShell's
// current URL as a single history entry. If the URL is not about:blank
@ -328,15 +355,13 @@ var SessionHistoryInternal = {
/**
* Restores session history data for a given docShell.
*
* @param docShell
* The docShell that owns the session history.
* @param history
* The session history object.
* @param tabData
* The tabdata including all history entries.
* @return A reference to the docShell's nsISHistory interface.
*/
restore(docShell, tabData) {
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let history = webNavigation.sessionHistory.legacySHistory;
restore(history, tabData) {
if (history.count > 0) {
history.PurgeHistory(history.count);
}