Source code

Revision control

Other Tools

/* 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 = ["SessionStore", "_LastSession"];
// Current version of the format used by Session Restore.
const FORMAT_VERSION = 1;
const TAB_CUSTOM_VALUES = new WeakMap();
const TAB_LAZY_STATES = new WeakMap();
const TAB_STATE_NEEDS_RESTORE = 1;
const TAB_STATE_RESTORING = 2;
const TAB_STATE_FOR_BROWSER = new WeakMap();
const WINDOW_RESTORE_IDS = new WeakMap();
const WINDOW_RESTORE_ZINDICES = new WeakMap();
const WINDOW_SHOWING_PROMISES = new Map();
// A new window has just been restored. At this stage, tabs are generally
// not restored.
const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
const NOTIFY_INITIATING_MANUAL_RESTORE =
"sessionstore-initiating-manual-restore";
const NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
const NOTIFY_DOMWINDOWCLOSED_HANDLED =
"sessionstore-debug-domwindowclosed-handled"; // WARNING: debug-only
const NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush";
// Maximum number of tabs to restore simultaneously. Previously controlled by
// the browser.sessionstore.max_concurrent_tabs pref.
const MAX_CONCURRENT_TAB_RESTORES = 3;
// Amount (in CSS px) by which we allow window edges to be off-screen
// when restoring a window, before we override the saved position to
// pull the window back within the available screen area.
const SCREEN_EDGE_SLOP = 8;
// global notifications observed
const OBSERVING = [
"browser-window-before-show",
"domwindowclosed",
"quit-application-granted",
"browser-lastwindow-close-granted",
"quit-application",
"browser:purge-session-history",
"browser:purge-session-history-for-domain",
"idle-daily",
"clear-origin-attributes-data",
"browsing-context-did-set-embedder",
"browsing-context-discarded",
"browser-shutdown-tabstate-updated",
];
// XUL Window properties to (re)store
// Restored in restoreDimensions()
const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
// Hideable window features to (re)store
// Restored in restoreWindowFeatures()
const WINDOW_HIDEABLE_FEATURES = [
"menubar",
"toolbar",
"locationbar",
"personalbar",
"statusbar",
"scrollbars",
];
// Messages that will be received via the Frame Message Manager.
const MESSAGES = [
// The content script sends us data that has been invalidated and needs to
// be saved to disk.
"SessionStore:update",
// The restoreHistory code has run. This is a good time to run SSTabRestoring.
"SessionStore:restoreHistoryComplete",
// The load for the restoring tab has begun. We update the URL bar at this
// time; if we did it before, the load would overwrite it.
"SessionStore:restoreTabContentStarted",
// All network loads for a restoring tab are done, so we should
// consider restoring another tab in the queue. The document has
// been restored, and forms have been filled. We trigger
// SSTabRestored at this time.
"SessionStore:restoreTabContentComplete",
// The content script encountered an error.
"SessionStore:error",
];
// The list of messages we accept from <xul:browser>s that have no tab
// assigned, or whose windows have gone away. Those are for example the
// ones that preload about:newtab pages, or from browsers where the window
// has just been closed.
const NOTAB_MESSAGES = new Set([
// For a description see above.
"SessionStore:update",
// For a description see above.
"SessionStore:error",
]);
// The list of messages we accept without an "epoch" parameter.
// See getCurrentEpoch() and friends to find out what an "epoch" is.
const NOEPOCH_MESSAGES = new Set([
// For a description see above.
"SessionStore:error",
]);
// The list of messages we want to receive even during the short period after a
// frame has been removed from the DOM and before its frame script has finished
// unloading.
const CLOSED_MESSAGES = new Set([
// For a description see above.
"SessionStore:update",
// For a description see above.
"SessionStore:error",
]);
// These are tab events that we listen to.
const TAB_EVENTS = [
"TabOpen",
"TabBrowserInserted",
"TabClose",
"TabSelect",
"TabShow",
"TabHide",
"TabPinned",
"TabUnpinned",
];
/**
* When calling restoreTabContent, we can supply a reason why
* the content is being restored. These are those reasons.
*/
const RESTORE_TAB_CONTENT_REASON = {
/**
* SET_STATE:
* We're restoring this tab's content because we're setting
* state inside this browser tab, probably because the user
* has asked us to restore a tab (or window, or entire session).
*/
SET_STATE: 0,
/**
* NAVIGATE_AND_RESTORE:
* We're restoring this tab's content because a navigation caused
* us to do a remoteness-flip.
*/
NAVIGATE_AND_RESTORE: 1,
};
// '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;
const { PrivateBrowsingUtils } = ChromeUtils.import(
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { TelemetryTimestamps } = ChromeUtils.import(
);
const { XPCOMUtils } = ChromeUtils.import(
);
ChromeUtils.defineModuleGetter(
this,
"SessionHistory",
);
XPCOMUtils.defineLazyServiceGetters(this, {
gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"],
});
XPCOMUtils.defineLazyModuleGetters(this, {
});
/**
* |true| if we are in debug mode, |false| otherwise.
* Debug mode is controlled by preference browser.sessionstore.debug
*/
var gDebuggingEnabled = false;
/**
* A global value to tell that fingerprinting resistance is enabled or not.
* If it's enabled, the session restore won't restore the window's size and
* size mode.
* This value is controlled by preference privacy.resistFingerprinting.
*/
var gResistFingerprintingEnabled = false;
var SessionStore = {
get promiseInitialized() {
return SessionStoreInternal.promiseInitialized;
},
get promiseAllWindowsRestored() {
return SessionStoreInternal.promiseAllWindowsRestored;
},
get canRestoreLastSession() {
return SessionStoreInternal.canRestoreLastSession;
},
set canRestoreLastSession(val) {
SessionStoreInternal.canRestoreLastSession = val;
},
get lastClosedObjectType() {
return SessionStoreInternal.lastClosedObjectType;
},
get willAutoRestore() {
return SessionStoreInternal.willAutoRestore;
},
init: function ss_init() {
SessionStoreInternal.init();
},
getBrowserState: function ss_getBrowserState() {
return SessionStoreInternal.getBrowserState();
},
setBrowserState: function ss_setBrowserState(aState) {
SessionStoreInternal.setBrowserState(aState);
},
getWindowState: function ss_getWindowState(aWindow) {
return SessionStoreInternal.getWindowState(aWindow);
},
setWindowState: function ss_setWindowState(aWindow, aState, aOverwrite) {
SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite);
},
getTabState: function ss_getTabState(aTab) {
return SessionStoreInternal.getTabState(aTab);
},
setTabState: function ss_setTabState(aTab, aState) {
SessionStoreInternal.setTabState(aTab, aState);
},
// Return whether a tab is restoring.
isTabRestoring(aTab) {
return TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser);
},
getInternalObjectState(obj) {
return SessionStoreInternal.getInternalObjectState(obj);
},
duplicateTab: function ss_duplicateTab(
aWindow,
aTab,
aDelta = 0,
aRestoreImmediately = true,
aOptions = {}
) {
return SessionStoreInternal.duplicateTab(
aWindow,
aTab,
aDelta,
aRestoreImmediately,
aOptions
);
},
getLastClosedTabCount(aWindow) {
return SessionStoreInternal.getLastClosedTabCount(aWindow);
},
setLastClosedTabCount(aWindow, aNumber) {
return SessionStoreInternal.setLastClosedTabCount(aWindow, aNumber);
},
getClosedTabCount: function ss_getClosedTabCount(aWindow) {
return SessionStoreInternal.getClosedTabCount(aWindow);
},
getClosedTabData: function ss_getClosedTabData(aWindow, aAsString = true) {
return SessionStoreInternal.getClosedTabData(aWindow, aAsString);
},
undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) {
return SessionStoreInternal.undoCloseTab(aWindow, aIndex);
},
forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
return SessionStoreInternal.forgetClosedTab(aWindow, aIndex);
},
getClosedWindowCount: function ss_getClosedWindowCount() {
return SessionStoreInternal.getClosedWindowCount();
},
getClosedWindowData: function ss_getClosedWindowData(aAsString = true) {
return SessionStoreInternal.getClosedWindowData(aAsString);
},
undoCloseWindow: function ss_undoCloseWindow(aIndex) {
return SessionStoreInternal.undoCloseWindow(aIndex);
},
forgetClosedWindow: function ss_forgetClosedWindow(aIndex) {
return SessionStoreInternal.forgetClosedWindow(aIndex);
},
getCustomWindowValue(aWindow, aKey) {
return SessionStoreInternal.getCustomWindowValue(aWindow, aKey);
},
setCustomWindowValue(aWindow, aKey, aStringValue) {
SessionStoreInternal.setCustomWindowValue(aWindow, aKey, aStringValue);
},
deleteCustomWindowValue(aWindow, aKey) {
SessionStoreInternal.deleteCustomWindowValue(aWindow, aKey);
},
getCustomTabValue(aTab, aKey) {
return SessionStoreInternal.getCustomTabValue(aTab, aKey);
},
setCustomTabValue(aTab, aKey, aStringValue) {
SessionStoreInternal.setCustomTabValue(aTab, aKey, aStringValue);
},
deleteCustomTabValue(aTab, aKey) {
SessionStoreInternal.deleteCustomTabValue(aTab, aKey);
},
getLazyTabValue(aTab, aKey) {
return SessionStoreInternal.getLazyTabValue(aTab, aKey);
},
getCustomGlobalValue(aKey) {
return SessionStoreInternal.getCustomGlobalValue(aKey);
},
setCustomGlobalValue(aKey, aStringValue) {
SessionStoreInternal.setCustomGlobalValue(aKey, aStringValue);
},
deleteCustomGlobalValue(aKey) {
SessionStoreInternal.deleteCustomGlobalValue(aKey);
},
persistTabAttribute: function ss_persistTabAttribute(aName) {
SessionStoreInternal.persistTabAttribute(aName);
},
restoreLastSession: function ss_restoreLastSession() {
SessionStoreInternal.restoreLastSession();
},
speculativeConnectOnTabHover(tab) {
SessionStoreInternal.speculativeConnectOnTabHover(tab);
},
getCurrentState(aUpdateAll) {
return SessionStoreInternal.getCurrentState(aUpdateAll);
},
reviveCrashedTab(aTab) {
return SessionStoreInternal.reviveCrashedTab(aTab);
},
reviveAllCrashedTabs() {
return SessionStoreInternal.reviveAllCrashedTabs();
},
updateSessionStoreFromTablistener(aBrowser, aBrowsingContext, aData) {
return SessionStoreInternal.updateSessionStoreFromTablistener(
aBrowser,
aBrowsingContext,
aData
);
},
getSessionHistory(tab, updatedCallback) {
return SessionStoreInternal.getSessionHistory(tab, updatedCallback);
},
undoCloseById(aClosedId, aIncludePrivate) {
return SessionStoreInternal.undoCloseById(aClosedId, aIncludePrivate);
},
resetBrowserToLazyState(tab) {
return SessionStoreInternal.resetBrowserToLazyState(tab);
},
maybeExitCrashedState(browser) {
SessionStoreInternal.maybeExitCrashedState(browser);
},
isBrowserInCrashedSet(browser) {
return SessionStoreInternal.isBrowserInCrashedSet(browser);
},
/**
* Ensures that session store has registered and started tracking a given window.
* @param window
* Window reference
*/
ensureInitialized(window) {
if (SessionStoreInternal._sessionInitialized && !window.__SSi) {
/*
We need to check that __SSi is not defined on the window so that if
onLoad function is in the middle of executing we don't enter the function
again and try to redeclare the ContentSessionStore script.
*/
SessionStoreInternal.onLoad(window);
}
},
getCurrentEpoch(browser) {
return SessionStoreInternal.getCurrentEpoch(browser);
},
/**
* Determines whether the passed version number is compatible with
* the current version number of the SessionStore.
*
* @param version The format and version of the file, as an array, e.g.
* ["sessionrestore", 1]
*/
isFormatVersionCompatible(version) {
if (!version) {
return false;
}
if (!Array.isArray(version)) {
// Improper format.
return false;
}
if (version[0] != "sessionrestore") {
// Not a Session Restore file.
return false;
}
let number = Number.parseFloat(version[1]);
if (Number.isNaN(number)) {
return false;
}
return number <= FORMAT_VERSION;
},
/**
* Filters out not worth-saving tabs from a given browser state object.
*
* @param aState (object)
* The browser state for which we remove worth-saving tabs.
* The given object will be modified.
*/
keepOnlyWorthSavingTabs(aState) {
for (let i = aState.windows.length - 1; i >= 0; i--) {
let win = aState.windows[i];
for (let j = win.tabs.length - 1; j >= 0; j--) {
let tab = win.tabs[j];
if (!SessionStoreInternal._shouldSaveTab(tab)) {
win.tabs.splice(j, 1);
if (win.selected > j) {
win.selected--;
}
}
}
if (!win.tabs.length) {
aState.windows.splice(i, 1);
if (aState.selectedWindow > i) {
aState.selectedWindow--;
}
}
}
},
/**
* Prepares to change the remoteness of the given browser, by ensuring that
* the local instance of session history is up-to-date.
*/
async prepareToChangeRemoteness(aTab) {
await SessionStoreInternal.prepareToChangeRemoteness(aTab);
},
finishTabRemotenessChange(aTab, aSwitchId) {
SessionStoreInternal.finishTabRemotenessChange(aTab, aSwitchId);
},
};
// Freeze the SessionStore object. We don't want anyone to modify it.
Object.freeze(SessionStore);
var SessionStoreInternal = {
QueryInterface: ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",
]),
_globalState: new GlobalState(),
// A counter to be used to generate a unique ID for each closed tab or window.
_nextClosedId: 0,
// During the initial restore and setBrowserState calls tracks the number of
// 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(),
// For each <browser> element that's being restored, holds a web progress
// listener that watches for STATE_START and STATE_STOP events.
_browserProgressListenerForRestore: 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(),
// Any browsers that fires the oop-browser-crashed event gets stored in
// here - that way we know which browsers to ignore messages from (until
// they get restored).
_crashedBrowsers: new WeakSet(),
// A map (xul:browser -> FrameLoader) that maps a browser to the last
// associated frameLoader we heard about.
_lastKnownFrameLoader: new WeakMap(),
// A map (xul:browser -> object) that maps a browser associated with a
// recently closed tab to all its necessary state information we need to
// properly handle final update message.
_closedTabs: new WeakMap(),
// A map (xul:browser -> object) that maps a browser associated with a
// recently closed tab due to a window closure to the tab state information
// that is being stored in _closedWindows for that tab.
_closedWindowTabs: new WeakMap(),
// A set of window data that has the potential to be saved in the _closedWindows
// array for the session. We will remove window data from this set whenever
// forgetClosedWindow is called for the window, or when session history is
// purged, so that we don't accidentally save that data after the flush has
// completed. Closed tabs use a more complicated mechanism for this particular
// problem. When forgetClosedTab is called, the browser is removed from the
// _closedTabs map, so its data is not recorded. In the purge history case,
// the closedTabs array per window is overwritten so that once the flush is
// complete, the tab would only ever add itself to an array that SessionStore
// no longer cares about. Bug 1230636 has been filed to make the tab case
// work more like the window case, which is more explicit, and easier to
// reason about.
_saveableClosedWindowData: new WeakSet(),
// whether a setBrowserState call is in progress
_browserSetState: false,
// time in milliseconds when the session was started (saved across sessions),
// defaults to now if no session was restored or timestamp doesn't exist
_sessionStartTime: Date.now(),
// states for all currently opened windows
_windows: {},
// counter for creating unique window IDs
_nextWindowID: 0,
// states for all recently closed windows
_closedWindows: [],
// collection of session states yet to be restored
_statesToRestore: {},
// counts the number of crashes since the last clean start
_recentCrashes: 0,
// whether the last window was closed and should be restored
_restoreLastWindow: false,
// number of tabs currently restoring
_tabsRestoringCount: 0,
_log: null,
// When starting Firefox with a single private window, this is the place
// where we keep the session we actually wanted to restore in case the user
// decides to later open a non-private window as well.
_deferredInitialState: null,
// Keeps track of whether a notification needs to be sent that closed objects have changed.
_closedObjectsChanged: false,
// A promise resolved once initialization is complete
_deferredInitialized: (function() {
let deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
})(),
// Whether session has been initialized
_sessionInitialized: false,
// A promise resolved once all windows are restored.
_deferredAllWindowsRestored: (function() {
let deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
})(),
get promiseAllWindowsRestored() {
return this._deferredAllWindowsRestored.promise;
},
// Promise that is resolved when we're ready to initialize
// and restore the session.
_promiseReadyForInitialization: null,
// Keep busy state counters per window.
_windowBusyStates: new WeakMap(),
/**
* A promise fulfilled once initialization is complete.
*/
get promiseInitialized() {
return this._deferredInitialized.promise;
},
get canRestoreLastSession() {
return LastSession.canRestore;
},
set canRestoreLastSession(val) {
// Cheat a bit; only allow false.
if (!val) {
LastSession.clear();
}
},
/**
* Returns a string describing the last closed object, either "tab" or "window".
*
* This was added to support the sessions.restore WebExtensions API.
*/
get lastClosedObjectType() {
if (this._closedWindows.length) {
// Since there are closed windows, we need to check if there's a closed tab
// in one of the currently open windows that was closed after the
// last-closed window.
let tabTimestamps = [];
for (let window of Services.wm.getEnumerator("navigator:browser")) {
let windowState = this._windows[window.__SSi];
if (windowState && windowState._closedTabs[0]) {
tabTimestamps.push(windowState._closedTabs[0].closedAt);
}
}
if (
!tabTimestamps.length ||
tabTimestamps.sort((a, b) => b - a)[0] < this._closedWindows[0].closedAt
) {
return "window";
}
}
return "tab";
},
/**
* Returns a boolean that determines whether the session will be automatically
* restored upon the _next_ startup or a restart.
*/
get willAutoRestore() {
return (
!PrivateBrowsingUtils.permanentPrivateBrowsing &&
(Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") ||
Services.prefs.getIntPref("browser.startup.page") ==
BROWSER_STARTUP_RESUME_SESSION)
);
},
/**
* Initialize the sessionstore service.
*/
init() {
if (this._initialized) {
throw new Error("SessionStore.init() must only be called once!");
}
TelemetryTimestamps.add("sessionRestoreInitialized");
OBSERVING.forEach(function(aTopic) {
Services.obs.addObserver(this, aTopic, true);
}, this);
this._initPrefs();
this._initialized = true;
this._closedTabCache = new WeakMap();
Services.telemetry
.getHistogramById("FX_SESSION_RESTORE_PRIVACY_LEVEL")
.add(Services.prefs.getIntPref("browser.sessionstore.privacy_level"));
},
/**
* Initialize the session using the state provided by SessionStartup
*/
initSession() {
TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
let state;
let ss = SessionStartup;
if (ss.willRestore() || ss.sessionType == ss.DEFER_SESSION) {
state = ss.state;
}
if (state) {
try {
// If we're doing a DEFERRED session, then we want to pull pinned tabs
// out so they can be restored.
if (ss.sessionType == ss.DEFER_SESSION) {
let [iniState, remainingState] = this._prepDataForDeferredRestore(
state
);
// If we have a iniState with windows, that means that we have windows
// with app tabs to restore.
if (iniState.windows.length) {
// Move cookies over from the remaining state so that they're
// restored right away, and pinned tabs will load correctly.
iniState.cookies = remainingState.cookies;
delete remainingState.cookies;
state = iniState;
} else {
state = null;
}
if (remainingState.windows.length) {
LastSession.setState(remainingState);
}
Services.telemetry.keyedScalarAdd(
"browser.engagement.sessionrestore_interstitial",
"deferred_restore",
1
);
} else {
// Get the last deferred session in case the user still wants to
// restore it
LastSession.setState(state.lastSessionState);
let restoreAsCrashed = ss.willRestoreAsCrashed();
if (restoreAsCrashed) {
this._recentCrashes =
((state.session && state.session.recentCrashes) || 0) + 1;
// _needsRestorePage will record sessionrestore_interstitial,
// including the specific reason we decided we needed to show
// about:sessionrestore, if that's what we do.
if (this._needsRestorePage(state, this._recentCrashes)) {
// replace the crashed session with a restore-page-only session
let url = "about:sessionrestore";
let formdata = { id: { sessionData: state }, url };
let entry = {
url,
triggeringPrincipal_base64:
E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
};
state = { windows: [{ tabs: [{ entries: [entry], formdata }] }] };
} else if (
this._hasSingleTabWithURL(state.windows, "about:welcomeback")
) {
Services.telemetry.keyedScalarAdd(
"browser.engagement.sessionrestore_interstitial",
"shown_only_about_welcomeback",
1
);
// On a single about:welcomeback URL that crashed, replace about:welcomeback
// with about:sessionrestore, to make clear to the user that we crashed.
state.windows[0].tabs[0].entries[0].url = "about:sessionrestore";
state.windows[0].tabs[0].entries[0].triggeringPrincipal_base64 =
E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
} else {
restoreAsCrashed = false;
}
}
// If we didn't use about:sessionrestore, record that:
if (!restoreAsCrashed) {
Services.telemetry.keyedScalarAdd(
"browser.engagement.sessionrestore_interstitial",
"autorestore",
1
);
}
// Update the session start time using the restored session state.
this._updateSessionStartTime(state);
// Make sure that at least the first window doesn't have anything hidden.
delete state.windows[0].hidden;
// Since nothing is hidden in the first window, it cannot be a popup.
delete state.windows[0].isPopup;
// We don't want to minimize and then open a window at startup.
if (state.windows[0].sizemode == "minimized") {
state.windows[0].sizemode = "normal";
}
// clear any lastSessionWindowID attributes since those don't matter
// during normal restore
state.windows.forEach(function(aWindow) {
delete aWindow.__lastSessionWindowID;
});
}
} catch (ex) {
this._log.error("The session file is invalid: " + ex);
}
}
// at this point, we've as good as resumed the session, so we can
// clear the resume_session_once flag, if it's set
if (
!RunState.isQuitting &&
this._prefBranch.getBoolPref("sessionstore.resume_session_once")
) {
this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
}
TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
return state;
},
_initPrefs() {
this._prefBranch = Services.prefs.getBranch("browser.");
gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
Services.prefs.addObserver("browser.sessionstore.debug", () => {
gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
});
this._log = console.createInstance({
prefix: "SessionStore",
maxLogLevel: gDebuggingEnabled ? "Debug" : "Warn",
});
this._max_tabs_undo = this._prefBranch.getIntPref(
"sessionstore.max_tabs_undo"
);
this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
this._max_windows_undo = this._prefBranch.getIntPref(
"sessionstore.max_windows_undo"
);
this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
this._restore_on_demand = this._prefBranch.getBoolPref(
"sessionstore.restore_on_demand"
);
this._prefBranch.addObserver("sessionstore.restore_on_demand", this, true);
gResistFingerprintingEnabled = Services.prefs.getBoolPref(
"privacy.resistFingerprinting"
);
Services.prefs.addObserver("privacy.resistFingerprinting", this);
this._shistoryInParent = Services.appinfo.sessionHistoryInParent;
},
/**
* Called on application shutdown, after notifications:
* quit-application-granted, quit-application
*/
_uninit: function ssi_uninit() {
if (!this._initialized) {
throw new Error("SessionStore is not initialized.");
}
// Prepare to close the session file and write the last state.
RunState.setClosing();
// save all data for session resuming
if (this._sessionInitialized) {
SessionSaver.run();
}
// clear out priority queue in case it's still holding refs
TabRestoreQueue.reset();
// Make sure to cancel pending saves.
SessionSaver.cancel();
},
/**
* Handle notifications
*/
observe: function ssi_observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "browser-window-before-show": // catch new windows
this.onBeforeBrowserWindowShown(aSubject);
break;
case "domwindowclosed": // catch closed windows
this.onClose(aSubject).then(() => {
this._notifyOfClosedObjectsChange();
});
if (gDebuggingEnabled) {
Services.obs.notifyObservers(null, NOTIFY_DOMWINDOWCLOSED_HANDLED);
}
break;
case "quit-application-granted":
let syncShutdown = aData == "syncShutdown";
this.onQuitApplicationGranted(syncShutdown);
break;
case "browser-lastwindow-close-granted":
this.onLastWindowCloseGranted();
break;
case "quit-application":
this.onQuitApplication(aData);
break;
case "browser:purge-session-history": // catch sanitization
this.onPurgeSessionHistory();
this._notifyOfClosedObjectsChange();
break;
case "browser:purge-session-history-for-domain":
this.onPurgeDomainData(aData);
this._notifyOfClosedObjectsChange();
break;
case "nsPref:changed": // catch pref changes
this.onPrefChange(aData);
this._notifyOfClosedObjectsChange();
break;
case "idle-daily":
this.onIdleDaily();
this._notifyOfClosedObjectsChange();
break;
case "clear-origin-attributes-data":
let userContextId = 0;
try {
userContextId = JSON.parse(aData).userContextId;
} catch (e) {}
if (userContextId) {
this._forgetTabsWithUserContextId(userContextId);
}
break;
case "browsing-context-did-set-embedder":
if (Services.appinfo.sessionHistoryInParent) {
if (
aSubject &&
aSubject === aSubject.top &&
aSubject.isContent &&
aSubject.embedderElement
) {
this.addSHistoryListener(aSubject.embedderElement, aSubject, true);
}
}
break;
case "browsing-context-discarded":
if (Services.appinfo.sessionHistoryInParent) {
let permanentKey = aSubject?.embedderElement?.permanentKey;
if (permanentKey) {
this._browserSHistoryListener.get(permanentKey)?.uninstall();
}
}
break;
case "browser-shutdown-tabstate-updated":
if (Services.appinfo.sessionHistoryInParent) {
// Non-SHIP code calls this when the frame script is unloaded.
this.onFinalTabStateUpdateComplete(aSubject);
}
break;
}
},
// Add a new SessionHistory listener to the provided browsing context and save
// a reference to that listener in the _browserSHistoryListener map.
addSHistoryListener(aBrowser, aBrowsingContext, aCollectImmediately = false) {
class SHistoryListener {
constructor() {
this._browserId = aBrowsingContext.browserId;
this._permanentKey = aBrowser.permanentKey;
this._fromIndex = kNoIndex;
}
uninstall() {
let bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId);
bc?.sessionHistory?.removeSHistoryListener(this);
SessionStoreInternal._browserSHistoryListener.delete(
this._permanentKey
);
}
collect(
browser,
browsingContext,
{ immediate = true, collectFull = true, writeToCache = false }
) {
if (!immediate) {
// Queue a tab state update on the |browser.sessionstore.interval|
// timer. We'll eventually call this again with |immediate=true|.
browser.frameLoader?.requestSHistoryUpdate();
return null;
}
// Don't bother doing anything if we haven't actually seen any history
// changes.
if (!collectFull && this._fromIndex === kNoIndex) {
return null;
}
let fromIndex = collectFull ? -1 : this._fromIndex;
this._fromIndex = kNoIndex;
let historychange = SessionHistory.collectFromParent(
browsingContext.currentURI?.spec,
true, // Bug 1704574
browsingContext.sessionHistory,
fromIndex
);
if (writeToCache) {
SessionStoreInternal.onTabStateUpdate(browser, {
data: { historychange },
});
}
return historychange;
}
collectFrom(index) {
if (this._fromIndex <= 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 cases we use kLastIndex which ignores
// only the subsequent navigations, but not any new elements added.
return;
}
let bc = BrowsingContext.getCurrentTopByBrowserId(this._browserId);
if (bc?.embedderElement) {
this._fromIndex = index;
this.collect(bc.embedderElement, bc, { immediate: false });
}
}
OnHistoryNewEntry(newURI, oldIndex) {
// We use oldIndex - 1 to collect the current entry as well. This makes
// sure to collect any changes that were made to the entry while the
// document was active.
this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1);
}
OnHistoryGotoIndex() {
this.collectFrom(kLastIndex);
}
OnHistoryPurge() {
this.collectFrom(-1);
}
OnHistoryReload() {
this.collectFrom(-1);
return true;
}
OnHistoryReplaceEntry() {
this.collectFrom(-1);
}
}
SHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([
"nsISHistoryListener",
"nsISupportsWeakReference",
]);
if (!Services.appinfo.sessionHistoryInParent) {
throw new Error("This function should only be used with SHIP");
}
let sessionHistory = aBrowsingContext.sessionHistory;
if (!aBrowser || !aBrowser.permanentKey || !sessionHistory) {
return null;
}
// XXX: Maybe investigate unregistering the previous listener?
if (this._browserSHistoryListener.has(aBrowser.permanentKey)) {
return null;
}
let listener = new SHistoryListener();
sessionHistory.addSHistoryListener(listener);
this._browserSHistoryListener.set(aBrowser.permanentKey, listener);
if (aCollectImmediately) {
let uri = aBrowsingContext.currentURI?.spec;
if (uri !== "about:blank" || sessionHistory.count !== 0) {
listener.collect(aBrowser, aBrowsingContext, { writeToCache: true });
}
}
return listener;
},
/**
* 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, aCallbacks) {
if (!Services.appinfo.sessionHistoryInParent) {
throw new Error("This function should only be used with SHIP");
}
function SHistoryListener(browser, callbacks) {
browser.browsingContext.sessionHistory.addSHistoryListener(this);
this.browser = browser;
this.callbacks = callbacks;
}
SHistoryListener.prototype = {
QueryInterface: ChromeUtils.generateQI([
"nsISHistoryListener",
"nsISupportsWeakReference",
]),
uninstall() {
let shistory = this.browser.browsingContext?.sessionHistory;
if (shistory) {
shistory.removeSHistoryListener(this);
}
SessionStoreInternal._browserSHistoryListenerForRestore.delete(
this.browser.permanentKey
);
},
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;
}
if (this.callbacks.onHistoryNewEntry) {
this.callbacks.onHistoryNewEntry(newURI);
}
},
OnHistoryReload() {
if (this.callbacks.onHistoryReload) {
return this.callbacks.onHistoryReload();
}
return false;
},
};
// XXX: When can this happen?
if (!aBrowser.browsingContext?.sessionHistory) {
throw new Error("no SessionHistory object");
}
// Ensure we only have 1 active listener per browser.
if (this._browserSHistoryListenerForRestore.has(aBrowser.permanentKey)) {
this._browserSHistoryListenerForRestore
.get(aBrowser.permanentKey)
.uninstall();
}
let listener = new SHistoryListener(aBrowser, aCallbacks);
this._browserSHistoryListenerForRestore.set(
aBrowser.permanentKey,
listener
);
},
addProgressListenerForRestore(browser, callbacks) {
if (!Services.appinfo.sessionHistoryInParent) {
throw new Error("This function should only be used with SHIP");
}
class ProgressListener {
constructor() {
browser.addProgressListener(
this,
Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
);
}
uninstall() {
browser.removeProgressListener(this);
SessionStoreInternal._browserProgressListenerForRestore.delete(
browser.permanentKey
);
}
onStateChange(webProgress, request, stateFlags, status) {
if (
webProgress.isTopLevel &&
stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
callbacks.onStopRequest
) {
callbacks.onStopRequest(request, this);
}
}
}
ProgressListener.prototype.QueryInterface = ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]);
// Ensure we only have 1 listener per browser.
if (this._browserProgressListenerForRestore.has(browser.permanentKey)) {
this._browserProgressListenerForRestore
.get(browser.permanentKey)
.uninstall();
}
let listener = new ProgressListener();
this._browserProgressListenerForRestore.set(browser.permanentKey, listener);
},
onTabStateUpdate(browser, data) {
// Ignore messages from <browser> elements that have crashed
// and not yet been revived.
if (!this._crashedBrowsers.has(browser.permanentKey)) {
TabState.update(browser, data);
this.saveStateDelayed(browser.ownerGlobal);
// Handle any updates sent by the child after the tab was closed. This
// might be the final update as sent by the "unload" handler but also
// any async update message that was sent before the child unloaded.
let closedTab = this._closedTabs.get(browser.permanentKey);
if (closedTab) {
// Update the closed tab's state. This will be reflected in its
// window's list of closed tabs as that refers to the same object.
TabState.copyFromCache(browser, closedTab.tabData.state);
}
}
},
onFinalTabStateUpdateComplete(browser) {
let permanentKey = browser.permanentKey;
if (
this._closedTabs.has(permanentKey) &&
!this._crashedBrowsers.has(permanentKey)
) {
let { closedTabs, tabData } = this._closedTabs.get(permanentKey);
// We expect no further updates.
this._closedTabs.delete(permanentKey);
// The tab state no longer needs this reference.
delete tabData.permanentKey;
// Determine whether the tab state is worth saving.
let shouldSave = this._shouldSaveTabState(tabData.state);
let index = closedTabs.indexOf(tabData);
if (shouldSave && index == -1) {
// If the tab state is worth saving and we didn't push it onto
// the list of closed tabs when it was closed (because we deemed
// the state not worth saving) then add it to the window's list
// of closed tabs now.
this.saveClosedTabData(closedTabs, tabData);
} else if (!shouldSave && index > -1) {
// Remove from the list of closed tabs. The update messages sent
// after the tab was closed changed enough state so that we no
// longer consider its data interesting enough to keep around.
this.removeClosedTabData(closedTabs, index);
}
}
// If this the final message we need to resolve all pending flush
// requests for the given browser as they might have been sent too
// 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);
this._browserSHistoryListener.get(permanentKey)?.uninstall();
this._browserSHistoryListenerForRestore.get(permanentKey)?.uninstall();
Services.obs.notifyObservers(browser, NOTIFY_BROWSER_SHUTDOWN_FLUSH);
},
updateSessionStoreFromTablistener(aBrowser, aBrowsingContext, aData) {
if (aBrowser.permanentKey == undefined) {
return;
}
// Ignore sessionStore update from previous epochs
if (!this.isCurrentEpoch(aBrowser, aData.epoch)) {
return;
}
if (
Services.appinfo.sessionHistoryInParent &&
aBrowsingContext === aBrowsingContext.top &&
aBrowsingContext.sessionHistory
) {
let listener =
this._browserSHistoryListener.get(aBrowser.permanentKey) ??
this.addSHistoryListener(aBrowser, aBrowsingContext);
let historychange = listener.collect(aBrowser, aBrowsingContext, {
collectFull: !!aData.sHistoryNeeded,
writeToCache: false,
});
if (historychange) {
aData.data.historychange = historychange;
}
}
this.onTabStateUpdate(aBrowser, aData);
},
/**
* This method handles incoming messages sent by the session store content
* script via the Frame Message Manager or Parent Process Message Manager,
* and thus enables communication with OOP tabs.
*/
receiveMessage(aMessage) {
// If we got here, that means we're dealing with a frame message
// manager message, so the target will be a <xul:browser>.
var browser = aMessage.target;
let win = browser.ownerGlobal;
let tab = win ? win.gBrowser.getTabForBrowser(browser) : null;
// Ensure we receive only specific messages from <xul:browser>s that
// have no tab or window assigned, e.g. the ones that preload
// about:newtab pages, or windows that have closed.
if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
throw new Error(
`received unexpected message '${aMessage.name}' ` +
`from a browser that has no tab or window`
);
}
let data = aMessage.data || {};
let hasEpoch = data.hasOwnProperty("epoch");
// Most messages sent by frame scripts require to pass an epoch.
if (!hasEpoch && !NOEPOCH_MESSAGES.has(aMessage.name)) {
throw new Error(`received message '${aMessage.name}' without an epoch`);
}
// Ignore messages from previous epochs.
if (hasEpoch && !this.isCurrentEpoch(browser, data.epoch)) {
return;
}
switch (aMessage.name) {
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
// frameLoader we know about to compare.
let frameLoader =
browser.frameLoader ||
this._lastKnownFrameLoader.get(browser.permanentKey);
// If the message isn't targeting the latest frameLoader discard it.
if (frameLoader != aMessage.targetFrameLoader) {
return;
}
this.onTabStateUpdate(browser, data);
if (data.isFinal) {
this.onFinalTabStateUpdateComplete(browser);
} else if (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
// consumer that's waiting for the flush to be done.
TabStateFlusher.resolve(browser, data.flushID);
}
break;
case "SessionStore:restoreHistoryComplete":
this._restoreHistoryComplete(browser, data);
break;
case "SessionStore:restoreTabContentStarted":
this._restoreTabContentStarted(browser, data);
break;
case "SessionStore:restoreTabContentComplete":
this._restoreTabContentComplete(browser, data);
break;
case "SessionStore:error":
TabStateFlusher.resolveAll(
browser,
false,
"Received error from the content process"
);
break;
default:
throw new Error(`received unknown message '${aMessage.name}'`);
}
},
/* ........ Window Event Handlers .............. */
/**
* Implement EventListener for handling various window and tab events
*/
handleEvent: function ssi_handleEvent(aEvent) {
let win = aEvent.currentTarget.ownerGlobal;
let target = aEvent.originalTarget;
switch (aEvent.type) {
case "TabOpen":
this.onTabAdd(win);
break;
case "TabBrowserInserted":
this.onTabBrowserInserted(win, target);
break;
case "TabClose":
// `adoptedBy` will be set if the tab was closed because it is being
// moved to a new window.
if (!aEvent.detail.adoptedBy) {
this.onTabClose(win, target);
}
this.onTabRemove(win, target);
this._notifyOfClosedObjectsChange();
break;
case "TabSelect":
this.onTabSelect(win);
break;
case "TabShow":
this.onTabShow(win, target);
break;
case "TabHide":
this.onTabHide(win, target);
break;
case "TabPinned":
case "TabUnpinned":
case "SwapDocShells":
this.saveStateDelayed(win);
break;
case "oop-browser-crashed":
case "oop-browser-buildid-mismatch":
if (aEvent.isTopFrame) {
this.onBrowserCrashed(target);
}
break;
case "XULFrameLoaderCreated":
if (
target.namespaceURI == XUL_NS &&
target.localName == "browser" &&
target.frameLoader &&
target.permanentKey
) {
this._lastKnownFrameLoader.set(
target.permanentKey,
target.frameLoader
);
this.resetEpoch(target);
}
break;
default:
throw new Error(`unhandled event ${aEvent.type}?`);
}
this._clearRestoringWindows();
},
/**
* Generate a unique window identifier
* @return string
* A unique string to identify a window
*/
_generateWindowID: function ssi_generateWindowID() {
return "window" + this._nextWindowID++;
},
/**
* Registers and tracks a given window.
*
* @param aWindow
* Window reference
*/
onLoad(aWindow) {
// return if window has already been initialized
if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi]) {
return;
}
// ignore windows opened while shutting down
if (RunState.isQuitting) {
return;
}
// Assign the window a unique identifier we can use to reference
// internal data about the window.
aWindow.__SSi = this._generateWindowID();
let mm = aWindow.getGroupMessageManager("browsers");
MESSAGES.forEach(msg => {
let listenWhenClosed = CLOSED_MESSAGES.has(msg);
mm.addMessageListener(msg, this, listenWhenClosed);
});
// Load the frame script after registering listeners.
if (!Services.appinfo.sessionHistoryInParent) {
mm.loadFrameScript(
true,
true
);
}
// and create its data object
this._windows[aWindow.__SSi] = {
tabs: [],
selected: 0,
_closedTabs: [],
busy: false,
};
if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
this._windows[aWindow.__SSi].isPrivate = true;
}
if (!this._isWindowLoaded(aWindow)) {
this._windows[aWindow.__SSi]._restoring = true;
}
if (!aWindow.toolbar.visible) {
this._windows[aWindow.__SSi].isPopup = true;
}
let tabbrowser = aWindow.gBrowser;
// add tab change listeners to all already existing tabs
for (let i = 0; i < tabbrowser.tabs.length; i++) {
this.onTabBrowserInserted(aWindow, tabbrowser.tabs[i]);
}
// notification of tab add/remove/selection/show/hide
TAB_EVENTS.forEach(function(aEvent) {
tabbrowser.tabContainer.addEventListener(aEvent, this, true);
}, this);
// Keep track of a browser's latest frameLoader.
aWindow.gBrowser.addEventListener("XULFrameLoaderCreated", this);
},
/**
* Initializes a given window.
*
* Windows are registered as soon as they are created but we need to wait for
* the session file to load, and the initial window's delayed startup to
* finish before initializing a window, i.e. restoring data into it.
*
* @param aWindow
* Window reference
* @param aInitialState
* The initial state to be loaded after startup (optional)
*/
initializeWindow(aWindow, aInitialState = null) {
let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
// perform additional initialization when the first window is loading
if (RunState.isStopped) {
RunState.setRunning();
// restore a crashed session resp. resume the last session if requested
if (aInitialState) {
// Don't write to disk right after startup. Set the last time we wrote
// to disk to NOW() to enforce a full interval before the next write.
SessionSaver.updateLastSaveTime();
if (isPrivateWindow) {
// We're starting with a single private window. Save the state we
// actually wanted to restore so that we can do it later in case
// the user opens another, non-private window.
this._deferredInitialState = SessionStartup.state;
// Nothing to restore now, notify observers things are complete.
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
Services.obs.notifyObservers(
null,
"sessionstore-one-or-no-tab-restored"
);
this._deferredAllWindowsRestored.resolve();
} else {
TelemetryTimestamps.add("sessionRestoreRestoring");
this._restoreCount = aInitialState.windows
? aInitialState.windows.length
: 0;
// global data must be restored before restoreWindow is called so that