diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2018-07-18 08:24:24 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2018-07-18 08:24:24 +0200 |
commit | fc61780b35af913801d72086456f493f63197da6 (patch) | |
tree | f85891288a7bd988da9f0f15ae64e5c63f00d493 /browser/components/sessionstore | |
parent | 69f7f9e5f1475891ce11cc4f431692f965b0cd30 (diff) | |
parent | 50d3e596bbe89c95615f96eb71f6bc5be737a1db (diff) | |
download | UXP-fc61780b35af913801d72086456f493f63197da6.tar UXP-fc61780b35af913801d72086456f493f63197da6.tar.gz UXP-fc61780b35af913801d72086456f493f63197da6.tar.lz UXP-fc61780b35af913801d72086456f493f63197da6.tar.xz UXP-fc61780b35af913801d72086456f493f63197da6.zip |
Merge commit '50d3e596bbe89c95615f96eb71f6bc5be737a1db' into Basilisk-releasev2018.07.18
# Conflicts:
# browser/app/profile/firefox.js
# browser/components/preferences/jar.mn
Diffstat (limited to 'browser/components/sessionstore')
33 files changed, 0 insertions, 11386 deletions
diff --git a/browser/components/sessionstore/ContentRestore.jsm b/browser/components/sessionstore/ContentRestore.jsm deleted file mode 100644 index d4972bcaf..000000000 --- a/browser/components/sessionstore/ContentRestore.jsm +++ /dev/null @@ -1,434 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["ContentRestore"]; - -const Cu = Components.utils; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); - -XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities", - "resource:///modules/sessionstore/DocShellCapabilities.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FormData", - "resource://gre/modules/FormData.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PageStyle", - "resource:///modules/sessionstore/PageStyle.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", - "resource://gre/modules/ScrollPosition.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", - "resource:///modules/sessionstore/SessionHistory.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", - "resource:///modules/sessionstore/SessionStorage.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Utils", - "resource://gre/modules/sessionstore/Utils.jsm"); - -/** - * This module implements the content side of session restoration. The chrome - * side is handled by SessionStore.jsm. The functions in this module are called - * by content-sessionStore.js based on messages received from SessionStore.jsm - * (or, in one case, based on a "load" event). Each tab has its own - * ContentRestore instance, constructed by content-sessionStore.js. - * - * In a typical restore, content-sessionStore.js will call the following based - * on messages and events it receives: - * - * restoreHistory(tabData, loadArguments, callbacks) - * Restores the tab's history and session cookies. - * restoreTabContent(loadArguments, finishCallback) - * Starts loading the data for the current page to restore. - * restoreDocument() - * Restore form and scroll data. - * - * When the page has been loaded from the network, we call finishCallback. It - * should send a message to SessionStore.jsm, which may cause other tabs to be - * restored. - * - * When the page has finished loading, a "load" event will trigger in - * content-sessionStore.js, which will call restoreDocument. At that point, - * form data is restored and the restore is complete. - * - * At any time, SessionStore.jsm can cancel the ongoing restore by sending a - * reset message, which causes resetRestore to be called. At that point it's - * legal to begin another restore. - */ -function ContentRestore(chromeGlobal) { - let internal = new ContentRestoreInternal(chromeGlobal); - let external = {}; - - let EXPORTED_METHODS = ["restoreHistory", - "restoreTabContent", - "restoreDocument", - "resetRestore" - ]; - - for (let method of EXPORTED_METHODS) { - external[method] = internal[method].bind(internal); - } - - return Object.freeze(external); -} - -function ContentRestoreInternal(chromeGlobal) { - this.chromeGlobal = chromeGlobal; - - // The following fields are only valid during certain phases of the restore - // process. - - // The tabData for the restore. Set in restoreHistory and removed in - // restoreTabContent. - this._tabData = null; - - // Contains {entry, pageStyle, scrollPositions, formdata}, where entry is a - // single entry from the tabData.entries array. Set in - // restoreTabContent and removed in restoreDocument. - this._restoringDocument = null; - - // This listener is used to detect reloads on restoring tabs. Set in - // restoreHistory and removed in restoreTabContent. - this._historyListener = null; - - // This listener detects when a pending tab starts loading (when not - // initiated by sessionstore) and when a restoring tab has finished loading - // data from the network. Set in restoreHistory() and restoreTabContent(), - // removed in resetRestore(). - this._progressListener = null; -} - -/** - * The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are - * public. - */ -ContentRestoreInternal.prototype = { - - get docShell() { - return this.chromeGlobal.docShell; - }, - - /** - * Starts the process of restoring a tab. The tabData to be restored is passed - * in here and used throughout the restoration. The epoch (which must be - * 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) { - this._tabData = tabData; - - // In case about:blank isn't done yet. - let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); - webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL); - - // Make sure currentURI is set so that switch-to-tab works before the tab is - // restored. We'll reset this to about:blank when we try to restore the tab - // to ensure that docshell doeesn't get confused. Don't bother doing this if - // we're restoring immediately due to a process switch. It just causes the - // URL bar to be temporarily blank. - let activeIndex = tabData.index - 1; - let activePageData = tabData.entries[activeIndex] || {}; - let uri = activePageData.url || null; - if (uri && !loadArguments) { - webNavigation.setCurrentURI(Utils.makeURI(uri)); - } - - SessionHistory.restore(this.docShell, tabData); - - // Add a listener to watch for reloads. - let listener = new HistoryListener(this.docShell, () => { - // On reload, restore tab contents. - this.restoreTabContent(null, false, callbacks.onLoadFinished); - }); - - webNavigation.sessionHistory.addSHistoryListener(listener); - this._historyListener = listener; - - // Make sure to reset the capabilities and attributes in case this tab gets - // reused. - let disallow = new Set(tabData.disallow && tabData.disallow.split(",")); - DocShellCapabilities.restore(this.docShell, disallow); - - if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) { - SessionStorage.restore(this.docShell, tabData.storage); - delete tabData.storage; - } - - // Add a progress listener to correctly handle browser.loadURI() - // calls from foreign code. - this._progressListener = new ProgressListener(this.docShell, { - onStartRequest: () => { - // Some code called browser.loadURI() on a pending tab. It's safe to - // assume we don't care about restoring scroll or form data. - this._tabData = null; - - // Listen for the tab to finish loading. - this.restoreTabContentStarted(callbacks.onLoadFinished); - - // Notify the parent. - callbacks.onLoadStarted(); - } - }); - }, - - /** - * 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: function (loadArguments, isRemotenessUpdate, finishCallback) { - let tabData = this._tabData; - this._tabData = null; - - let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); - let history = webNavigation.sessionHistory; - - // Listen for the tab to finish loading. - this.restoreTabContentStarted(finishCallback); - - // 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 - // load happens. Don't bother doing this if we're restoring immediately - // due to a process switch. - if (!isRemotenessUpdate) { - webNavigation.setCurrentURI(Utils.makeURI("about:blank")); - } - - try { - if (loadArguments) { - // A load has been redirected to a new process so get history into the - // same state it was before the load started then trigger the load. - let referrer = loadArguments.referrer ? - Utils.makeURI(loadArguments.referrer) : null; - let referrerPolicy = ('referrerPolicy' in loadArguments - ? loadArguments.referrerPolicy - : Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT); - let postData = loadArguments.postData ? - Utils.makeInputStream(loadArguments.postData) : null; - let triggeringPrincipal = loadArguments.triggeringPrincipal - ? Utils.deserializePrincipal(loadArguments.triggeringPrincipal) - : null; - - if (loadArguments.userContextId) { - webNavigation.setOriginAttributesBeforeLoading({ userContextId: loadArguments.userContextId }); - } - - webNavigation.loadURIWithOptions(loadArguments.uri, loadArguments.flags, - referrer, referrerPolicy, postData, - null, null, triggeringPrincipal); - } else if (tabData.userTypedValue && tabData.userTypedClear) { - // If the user typed a URL into the URL bar and hit enter right before - // we crashed, we want to start loading that page again. A non-zero - // userTypedClear value means that the load had started. - // Load userTypedValue and fix up the URL if it's partial/broken. - webNavigation.loadURI(tabData.userTypedValue, - Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, - null, null, null); - } else if (tabData.entries.length) { - // Stash away the data we need for restoreDocument. - let activeIndex = tabData.index - 1; - this._restoringDocument = {entry: tabData.entries[activeIndex] || {}, - formdata: tabData.formdata || {}, - pageStyle: tabData.pageStyle || {}, - scrollPositions: tabData.scroll || {}}; - - // 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(); - } else { - // If there's nothing to restore, we should still blank the page. - webNavigation.loadURI("about:blank", - Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, - null, null, null); - } - - return true; - } catch (ex if ex instanceof Ci.nsIException) { - // Ignore page load errors, but return false to signal that the load never - // happened. - return false; - } - }, - - /** - * To be called after restoreHistory(). Removes all listeners needed for - * pending tabs and makes sure to notify when the tab finished loading. - */ - restoreTabContentStarted(finishCallback) { - // The reload listener is no longer needed. - this._historyListener.uninstall(); - this._historyListener = null; - - // Remove the old progress listener. - this._progressListener.uninstall(); - - // We're about to start a load. This listener will be called when the load - // has finished getting everything from the network. - this._progressListener = new ProgressListener(this.docShell, { - onStopRequest: () => { - // Call resetRestore() to reset the state back to normal. The data - // needed for restoreDocument() (which hasn't happened yet) will - // remain in _restoringDocument. - this.resetRestore(); - - finishCallback(); - } - }); - }, - - /** - * Finish restoring the tab by filling in form data and setting the scroll - * position. The restore is complete when this function exits. It should be - * called when the "load" event fires for the restoring tab. - */ - restoreDocument: function () { - if (!this._restoringDocument) { - return; - } - let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument; - this._restoringDocument = null; - - let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - - PageStyle.restoreTree(this.docShell, pageStyle); - FormData.restoreTree(window, formdata); - ScrollPosition.restoreTree(window, scrollPositions); - }, - - /** - * Cancel an ongoing restore. This function can be called any time between - * restoreHistory and restoreDocument. - * - * This function is called externally (if a restore is canceled) and - * internally (when the loads for a restore have finished). In the latter - * case, it's called before restoreDocument, so it cannot clear - * _restoringDocument. - */ - resetRestore: function () { - this._tabData = null; - - if (this._historyListener) { - this._historyListener.uninstall(); - } - this._historyListener = null; - - if (this._progressListener) { - this._progressListener.uninstall(); - } - this._progressListener = null; - } -}; - -/* - * 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. - */ -function HistoryListener(docShell, callback) { - let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); - webNavigation.sessionHistory.addSHistoryListener(this); - - this.webNavigation = webNavigation; - this.callback = callback; -} -HistoryListener.prototype = { - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsISHistoryListener, - Ci.nsISupportsWeakReference - ]), - - uninstall: function () { - let shistory = this.webNavigation.sessionHistory; - if (shistory) { - shistory.removeSHistoryListener(this); - } - }, - - OnHistoryGoBack: function(backURI) { return true; }, - OnHistoryGoForward: function(forwardURI) { return true; }, - OnHistoryGotoIndex: function(index, gotoURI) { return true; }, - OnHistoryPurge: function(numEntries) { return true; }, - OnHistoryReplaceEntry: function(index) {}, - - // 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.webNavigation.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.spec == newURI.spec)) { - 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(Utils.makeURI("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 flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; - this.webNavigation.loadURI(newURI.spec, flags, null, null, null); - }, - - OnHistoryReload(reloadURI, reloadFlags) { - this.callback(); - - // Cancel the load. - return false; - }, -} - -/** - * This class informs SessionStore.jsm whenever the network requests for a - * restoring page have completely finished. We only restore three tabs - * simultaneously, so this is the signal for SessionStore.jsm to kick off - * another restore (if there are more to do). - * - * The progress listener is also used to be notified when a load not initiated - * by sessionstore starts. Pending tabs will then need to be marked as no - * longer pending. - */ -function ProgressListener(docShell, callbacks) { - let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW); - - this.webProgress = webProgress; - this.callbacks = callbacks; -} - -ProgressListener.prototype = { - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference - ]), - - uninstall: function() { - this.webProgress.removeProgressListener(this); - }, - - onStateChange: function(webProgress, request, stateFlags, status) { - let {STATE_IS_WINDOW, STATE_STOP, STATE_START} = Ci.nsIWebProgressListener; - if (!webProgress.isTopLevel || !(stateFlags & STATE_IS_WINDOW)) { - return; - } - - if (stateFlags & STATE_START && this.callbacks.onStartRequest) { - this.callbacks.onStartRequest(); - } - - if (stateFlags & STATE_STOP && this.callbacks.onStopRequest) { - this.callbacks.onStopRequest(); - } - }, - - onLocationChange: function() {}, - onProgressChange: function() {}, - onStatusChange: function() {}, - onSecurityChange: function() {}, -}; diff --git a/browser/components/sessionstore/DocShellCapabilities.jsm b/browser/components/sessionstore/DocShellCapabilities.jsm deleted file mode 100644 index 098aae86f..000000000 --- a/browser/components/sessionstore/DocShellCapabilities.jsm +++ /dev/null @@ -1,50 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["DocShellCapabilities"]; - -/** - * The external API exported by this module. - */ -this.DocShellCapabilities = Object.freeze({ - collect: function (docShell) { - return DocShellCapabilitiesInternal.collect(docShell); - }, - - restore: function (docShell, disallow) { - return DocShellCapabilitiesInternal.restore(docShell, disallow); - }, -}); - -/** - * Internal functionality to save and restore the docShell.allow* properties. - */ -var DocShellCapabilitiesInternal = { - // List of docShell capabilities to (re)store. These are automatically - // retrieved from a given docShell if not already collected before. - // This is made so they're automatically in sync with all nsIDocShell.allow* - // properties. - caps: null, - - allCapabilities: function (docShell) { - if (!this.caps) { - let keys = Object.keys(docShell); - this.caps = keys.filter(k => k.startsWith("allow")).map(k => k.slice(5)); - } - return this.caps; - }, - - collect: function (docShell) { - let caps = this.allCapabilities(docShell); - return caps.filter(cap => !docShell["allow" + cap]); - }, - - restore: function (docShell, disallow) { - let caps = this.allCapabilities(docShell); - for (let cap of caps) - docShell["allow" + cap] = !disallow.has(cap); - }, -}; diff --git a/browser/components/sessionstore/FrameTree.jsm b/browser/components/sessionstore/FrameTree.jsm deleted file mode 100644 index e8ed12a8f..000000000 --- a/browser/components/sessionstore/FrameTree.jsm +++ /dev/null @@ -1,254 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["FrameTree"]; - -const Cu = Components.utils; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); - -const EXPORTED_METHODS = ["addObserver", "contains", "map", "forEach"]; - -/** - * A FrameTree represents all frames that were reachable when the document - * was loaded. We use this information to ignore frames when collecting - * sessionstore data as we can't currently restore anything for frames that - * have been created dynamically after or at the load event. - * - * @constructor - */ -function FrameTree(chromeGlobal) { - let internal = new FrameTreeInternal(chromeGlobal); - let external = {}; - - for (let method of EXPORTED_METHODS) { - external[method] = internal[method].bind(internal); - } - - return Object.freeze(external); -} - -/** - * The internal frame tree API that the public one points to. - * - * @constructor - */ -function FrameTreeInternal(chromeGlobal) { - // A WeakMap that uses frames (DOMWindows) as keys and their initial indices - // in their parents' child lists as values. Suppose we have a root frame with - // three subframes i.e. a page with three iframes. The WeakMap would have - // four entries and look as follows: - // - // root -> 0 - // subframe1 -> 0 - // subframe2 -> 1 - // subframe3 -> 2 - // - // Should one of the subframes disappear we will stop collecting data for it - // as |this._frames.has(frame) == false|. All other subframes will maintain - // their initial indices to ensure we can restore frame data appropriately. - this._frames = new WeakMap(); - - // The Set of observers that will be notified when the frame changes. - this._observers = new Set(); - - // The chrome global we use to retrieve the current DOMWindow. - this._chromeGlobal = chromeGlobal; - - // Register a web progress listener to be notified about new page loads. - let docShell = chromeGlobal.docShell; - let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor); - let webProgress = ifreq.getInterface(Ci.nsIWebProgress); - webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); -} - -FrameTreeInternal.prototype = { - - // Returns the docShell's current global. - get content() { - return this._chromeGlobal.content; - }, - - /** - * Adds a given observer |obs| to the set of observers that will be notified - * when the frame tree is reset (when a new document starts loading) or - * recollected (when a document finishes loading). - * - * @param obs (object) - */ - addObserver: function (obs) { - this._observers.add(obs); - }, - - /** - * Notifies all observers that implement the given |method|. - * - * @param method (string) - */ - notifyObservers: function (method) { - for (let obs of this._observers) { - if (obs.hasOwnProperty(method)) { - obs[method](); - } - } - }, - - /** - * Checks whether a given |frame| is contained in the collected frame tree. - * If it is not, this indicates that we should not collect data for it. - * - * @param frame (nsIDOMWindow) - * @return bool - */ - contains: function (frame) { - return this._frames.has(frame); - }, - - /** - * Recursively applies the given function |cb| to the stored frame tree. Use - * this method to collect sessionstore data for all reachable frames stored - * in the frame tree. - * - * If a given function |cb| returns a value, it must be an object. It may - * however return "null" to indicate that there is no data to be stored for - * the given frame. - * - * The object returned by |cb| cannot have any property named "children" as - * that is used to store information about subframes in the tree returned - * by |map()| and might be overridden. - * - * @param cb (function) - * @return object - */ - map: function (cb) { - let frames = this._frames; - - function walk(frame) { - let obj = cb(frame) || {}; - - if (frames.has(frame)) { - let children = []; - - Array.forEach(frame.frames, subframe => { - // Don't collect any data if the frame is not contained in the - // initial frame tree. It's a dynamic frame added later. - if (!frames.has(subframe)) { - return; - } - - // Retrieve the frame's original position in its parent's child list. - let index = frames.get(subframe); - - // Recursively collect data for the current subframe. - let result = walk(subframe, cb); - if (result && Object.keys(result).length) { - children[index] = result; - } - }); - - if (children.length) { - obj.children = children; - } - } - - return Object.keys(obj).length ? obj : null; - } - - return walk(this.content); - }, - - /** - * Applies the given function |cb| to all frames stored in the tree. Use this - * method if |map()| doesn't suit your needs and you want more control over - * how data is collected. - * - * @param cb (function) - * This callback receives the current frame as the only argument. - */ - forEach: function (cb) { - let frames = this._frames; - - function walk(frame) { - cb(frame); - - if (!frames.has(frame)) { - return; - } - - Array.forEach(frame.frames, subframe => { - if (frames.has(subframe)) { - cb(subframe); - } - }); - } - - walk(this.content); - }, - - /** - * Stores a given |frame| and its children in the frame tree. - * - * @param frame (nsIDOMWindow) - * @param index (int) - * The index in the given frame's parent's child list. - */ - collect: function (frame, index = 0) { - // Mark the given frame as contained in the frame tree. - this._frames.set(frame, index); - - // Mark the given frame's subframes as contained in the tree. - Array.forEach(frame.frames, this.collect, this); - }, - - /** - * @see nsIWebProgressListener.onStateChange - * - * We want to be notified about: - * - new documents that start loading to clear the current frame tree; - * - completed document loads to recollect reachable frames. - */ - onStateChange: function (webProgress, request, stateFlags, status) { - // Ignore state changes for subframes because we're only interested in the - // top-document starting or stopping its load. We thus only care about any - // changes to the root of the frame tree, not to any of its nodes/leafs. - if (!webProgress.isTopLevel || webProgress.DOMWindow != this.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._chromeGlobal.docShell.hasLoadedNonBlankURI) { - return; - } - - if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { - // Clear the list of frames until we can recollect it. - this._frames = new WeakMap(); - - // Notify observers that the frame tree has been reset. - this.notifyObservers("onFrameTreeReset"); - } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { - // The document and its resources have finished loading. - this.collect(webProgress.DOMWindow); - - // Notify observers that the frame tree has been reset. - this.notifyObservers("onFrameTreeCollected"); - } - }, - - // Unused nsIWebProgressListener methods. - onLocationChange: function () {}, - onProgressChange: function () {}, - onSecurityChange: function () {}, - onStatusChange: function () {}, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference]) -}; diff --git a/browser/components/sessionstore/GlobalState.jsm b/browser/components/sessionstore/GlobalState.jsm deleted file mode 100644 index ac2d7c81b..000000000 --- a/browser/components/sessionstore/GlobalState.jsm +++ /dev/null @@ -1,84 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["GlobalState"]; - -const EXPORTED_METHODS = ["getState", "clear", "get", "set", "delete", "setFromState"]; -/** - * Module that contains global session data. - */ -function GlobalState() { - let internal = new GlobalStateInternal(); - let external = {}; - for (let method of EXPORTED_METHODS) { - external[method] = internal[method].bind(internal); - } - return Object.freeze(external); -} - -function GlobalStateInternal() { - // Storage for global state. - this.state = {}; -} - -GlobalStateInternal.prototype = { - /** - * Get all value from the global state. - */ - getState: function() { - return this.state; - }, - - /** - * Clear all currently stored global state. - */ - clear: function() { - this.state = {}; - }, - - /** - * Retrieve a value from the global state. - * - * @param aKey - * A key the value is stored under. - * @return The value stored at aKey, or an empty string if no value is set. - */ - get: function(aKey) { - return this.state[aKey] || ""; - }, - - /** - * Set a global value. - * - * @param aKey - * A key to store the value under. - */ - set: function(aKey, aStringValue) { - this.state[aKey] = aStringValue; - }, - - /** - * Delete a global value. - * - * @param aKey - * A key to delete the value for. - */ - delete: function(aKey) { - delete this.state[aKey]; - }, - - /** - * Set the current global state from a state object. Any previous global - * state will be removed, even if the new state does not contain a matching - * key. - * - * @param aState - * A state object to extract global state from to be set. - */ - setFromState: function (aState) { - this.state = (aState && aState.global) || {}; - } -}; diff --git a/browser/components/sessionstore/PageStyle.jsm b/browser/components/sessionstore/PageStyle.jsm deleted file mode 100644 index 0424ef6b1..000000000 --- a/browser/components/sessionstore/PageStyle.jsm +++ /dev/null @@ -1,100 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["PageStyle"]; - -const Ci = Components.interfaces; - -/** - * The external API exported by this module. - */ -this.PageStyle = Object.freeze({ - collect: function (docShell, frameTree) { - return PageStyleInternal.collect(docShell, frameTree); - }, - - restoreTree: function (docShell, data) { - PageStyleInternal.restoreTree(docShell, data); - } -}); - -// Signifies that author style level is disabled for the page. -const NO_STYLE = "_nostyle"; - -var PageStyleInternal = { - /** - * Collects the selected style sheet sets for all reachable frames. - */ - collect: function (docShell, frameTree) { - let result = frameTree.map(({document: doc}) => { - let style; - - if (doc) { - // http://dev.w3.org/csswg/cssom/#persisting-the-selected-css-style-sheet-set - style = doc.selectedStyleSheetSet || doc.lastStyleSheetSet; - } - - return style ? {pageStyle: style} : null; - }); - - let markupDocumentViewer = - docShell.contentViewer; - - if (markupDocumentViewer.authorStyleDisabled) { - result = result || {}; - result.disabled = true; - } - - return result && Object.keys(result).length ? result : null; - }, - - /** - * Restores pageStyle data for the current frame hierarchy starting at the - * |docShell's| current DOMWindow using the given pageStyle |data|. - * - * Warning: If the current frame hierarchy doesn't match that of the given - * |data| object we will silently discard data for unreachable frames. We may - * as well assign page styles to the wrong frames if some were reordered or - * removed. - * - * @param docShell (nsIDocShell) - * @param data (object) - * { - * disabled: true, // when true, author styles will be disabled - * pageStyle: "Dusk", - * children: [ - * null, - * {pageStyle: "Mozilla", children: [ ... ]} - * ] - * } - */ - restoreTree: function (docShell, data) { - let disabled = data.disabled || false; - let markupDocumentViewer = - docShell.contentViewer; - markupDocumentViewer.authorStyleDisabled = disabled; - - function restoreFrame(root, data) { - if (data.hasOwnProperty("pageStyle")) { - root.document.selectedStyleSheetSet = data.pageStyle; - } - - if (!data.hasOwnProperty("children")) { - return; - } - - let frames = root.frames; - data.children.forEach((child, index) => { - if (child && index < frames.length) { - restoreFrame(frames[index], child); - } - }); - } - - let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor); - restoreFrame(ifreq.getInterface(Ci.nsIDOMWindow), data); - } -}; diff --git a/browser/components/sessionstore/PrivacyFilter.jsm b/browser/components/sessionstore/PrivacyFilter.jsm deleted file mode 100644 index 88713b402..000000000 --- a/browser/components/sessionstore/PrivacyFilter.jsm +++ /dev/null @@ -1,135 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["PrivacyFilter"]; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); - -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", - "resource:///modules/sessionstore/PrivacyLevel.jsm"); - -/** - * A module that provides methods to filter various kinds of data collected - * from a tab by the current privacy level as set by the user. - */ -this.PrivacyFilter = Object.freeze({ - /** - * Filters the given (serialized) session storage |data| according to the - * current privacy level and returns a new object containing only data that - * we're allowed to store. - * - * @param data The session storage data as collected from a tab. - * @return object - */ - filterSessionStorageData: function (data) { - let retval = {}; - - for (let host of Object.keys(data)) { - if (PrivacyLevel.check(host)) { - retval[host] = data[host]; - } - } - - return Object.keys(retval).length ? retval : null; - }, - - /** - * Filters the given (serialized) form |data| according to the current - * privacy level and returns a new object containing only data that we're - * allowed to store. - * - * @param data The form data as collected from a tab. - * @return object - */ - filterFormData: function (data) { - // If the given form data object has an associated URL that we are not - // allowed to store data for, bail out. We explicitly discard data for any - // children as well even if storing data for those frames would be allowed. - if (data.url && !PrivacyLevel.check(data.url)) { - return; - } - - let retval = {}; - - for (let key of Object.keys(data)) { - if (key === "children") { - let recurse = child => this.filterFormData(child); - let children = data.children.map(recurse).filter(child => child); - - if (children.length) { - retval.children = children; - } - // Only copy keys other than "children" if we have a valid URL in - // data.url and we thus passed the privacy level check. - } else if (data.url) { - retval[key] = data[key]; - } - } - - return Object.keys(retval).length ? retval : null; - }, - - /** - * Removes any private windows and tabs from a given browser state object. - * - * @param browserState (object) - * The browser state for which we remove any private windows and tabs. - * The given object will be modified. - */ - filterPrivateWindowsAndTabs: function (browserState) { - // Remove private opened windows. - for (let i = browserState.windows.length - 1; i >= 0; i--) { - let win = browserState.windows[i]; - - if (win.isPrivate) { - browserState.windows.splice(i, 1); - - if (browserState.selectedWindow >= i) { - browserState.selectedWindow--; - } - } else { - // Remove private tabs from all open non-private windows. - this.filterPrivateTabs(win); - } - } - - // Remove private closed windows. - browserState._closedWindows = - browserState._closedWindows.filter(win => !win.isPrivate); - - // Remove private tabs from all remaining closed windows. - browserState._closedWindows.forEach(win => this.filterPrivateTabs(win)); - }, - - /** - * Removes open private tabs from a given window state object. - * - * @param winState (object) - * The window state for which we remove any private tabs. - * The given object will be modified. - */ - filterPrivateTabs: function (winState) { - // Remove open private tabs. - for (let i = winState.tabs.length - 1; i >= 0 ; i--) { - let tab = winState.tabs[i]; - - if (tab.isPrivate) { - winState.tabs.splice(i, 1); - - if (winState.selected >= i) { - winState.selected--; - } - } - } - - // Note that closed private tabs are only stored for private windows. - // There is no need to call this function for private windows as the - // whole window state should just be discarded so we explicitly don't - // try to remove closed private tabs as an optimization. - } -}); diff --git a/browser/components/sessionstore/PrivacyLevel.jsm b/browser/components/sessionstore/PrivacyLevel.jsm deleted file mode 100644 index 135f1f959..000000000 --- a/browser/components/sessionstore/PrivacyLevel.jsm +++ /dev/null @@ -1,64 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["PrivacyLevel"]; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/Services.jsm"); - -const PREF = "browser.sessionstore.privacy_level"; - -// The following constants represent the different possible privacy levels that -// can be set by the user and that we need to consider when collecting text -// data, and cookies. -// -// Collect data from all sites (http and https). -const PRIVACY_NONE = 0; -// Collect data from unencrypted sites (http), only. -const PRIVACY_ENCRYPTED = 1; -// Collect no data. -const PRIVACY_FULL = 2; - -/** - * The external API as exposed by this module. - */ -var PrivacyLevel = Object.freeze({ - /** - * Returns whether the current privacy level allows saving data for the given - * |url|. - * - * @param url The URL we want to save data for. - * @return bool - */ - check: function (url) { - return PrivacyLevel.canSave({ isHttps: url.startsWith("https:") }); - }, - - /** - * Checks whether we're allowed to save data for a specific site. - * - * @param {isHttps: boolean} - * An object that must have one property: 'isHttps'. - * 'isHttps' tells whether the site us secure communication (HTTPS). - * @return {bool} Whether we can save data for the specified site. - */ - canSave: function ({isHttps}) { - let level = Services.prefs.getIntPref(PREF); - - // Never save any data when full privacy is requested. - if (level == PRIVACY_FULL) { - return false; - } - - // Don't save data for encrypted sites when requested. - if (isHttps && level == PRIVACY_ENCRYPTED) { - return false; - } - - return true; - } -}); diff --git a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm deleted file mode 100644 index ac5731160..000000000 --- a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm +++ /dev/null @@ -1,214 +0,0 @@ -/* 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/. */ - -this.EXPORTED_SYMBOLS = ["RecentlyClosedTabsAndWindowsMenuUtils"]; - -const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - -var Ci = Components.interfaces; -var Cc = Components.classes; -var Cr = Components.results; -var Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/PlacesUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", - "resource://gre/modules/PluralForm.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", - "resource:///modules/sessionstore/SessionStore.jsm"); - -var navigatorBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); - -this.RecentlyClosedTabsAndWindowsMenuUtils = { - - /** - * Builds up a document fragment of UI items for the recently closed tabs. - * @param aWindow - * The window that the tabs were closed in. - * @param aTagName - * The tag name that will be used when creating the UI items. - * @param aPrefixRestoreAll (defaults to false) - * Whether the 'restore all tabs' item is suffixed or prefixed to the list. - * If suffixed (the default) a separator will be inserted before it. - * @param aRestoreAllLabel (defaults to "menuRestoreAllTabs.label") - * Which localizable string to use for the 'restore all tabs' item. - * @returns A document fragment with UI items for each recently closed tab. - */ - getTabsFragment: function(aWindow, aTagName, aPrefixRestoreAll=false, - aRestoreAllLabel="menuRestoreAllTabs.label") { - let doc = aWindow.document; - let fragment = doc.createDocumentFragment(); - if (SessionStore.getClosedTabCount(aWindow) != 0) { - let closedTabs = SessionStore.getClosedTabData(aWindow, false); - for (let i = 0; i < closedTabs.length; i++) { - createEntry(aTagName, false, i, closedTabs[i], doc, - closedTabs[i].title, fragment); - } - - createRestoreAllEntry(doc, fragment, aPrefixRestoreAll, false, - aRestoreAllLabel, closedTabs.length, aTagName) - } - return fragment; - }, - - /** - * Builds up a document fragment of UI items for the recently closed windows. - * @param aWindow - * A window that can be used to create the elements and document fragment. - * @param aTagName - * The tag name that will be used when creating the UI items. - * @param aPrefixRestoreAll (defaults to false) - * Whether the 'restore all windows' item is suffixed or prefixed to the list. - * If suffixed (the default) a separator will be inserted before it. - * @param aRestoreAllLabel (defaults to "menuRestoreAllWindows.label") - * Which localizable string to use for the 'restore all windows' item. - * @returns A document fragment with UI items for each recently closed window. - */ - getWindowsFragment: function(aWindow, aTagName, aPrefixRestoreAll=false, - aRestoreAllLabel="menuRestoreAllWindows.label") { - let closedWindowData = SessionStore.getClosedWindowData(false); - let doc = aWindow.document; - let fragment = doc.createDocumentFragment(); - if (closedWindowData.length != 0) { - let menuLabelString = navigatorBundle.GetStringFromName("menuUndoCloseWindowLabel"); - let menuLabelStringSingleTab = - navigatorBundle.GetStringFromName("menuUndoCloseWindowSingleTabLabel"); - - for (let i = 0; i < closedWindowData.length; i++) { - let undoItem = closedWindowData[i]; - let otherTabsCount = undoItem.tabs.length - 1; - let label = (otherTabsCount == 0) ? menuLabelStringSingleTab - : PluralForm.get(otherTabsCount, menuLabelString); - let menuLabel = label.replace("#1", undoItem.title) - .replace("#2", otherTabsCount); - let selectedTab = undoItem.tabs[undoItem.selected - 1]; - - createEntry(aTagName, true, i, selectedTab, doc, menuLabel, - fragment); - } - - createRestoreAllEntry(doc, fragment, aPrefixRestoreAll, true, - aRestoreAllLabel, closedWindowData.length, - aTagName); - } - return fragment; - }, - - - /** - * Re-open a closed tab and put it to the end of the tab strip. - * Used for a middle click. - * @param aEvent - * The event when the user clicks the menu item - */ - _undoCloseMiddleClick: function(aEvent) { - if (aEvent.button != 1) - return; - - aEvent.view.undoCloseTab(aEvent.originalTarget.getAttribute("value")); - aEvent.view.gBrowser.moveTabToEnd(); - }, -}; - -function setImage(aItem, aElement) { - let iconURL = aItem.image; - // don't initiate a connection just to fetch a favicon (see bug 467828) - if (/^https?:/.test(iconURL)) - iconURL = "moz-anno:favicon:" + iconURL; - - aElement.setAttribute("image", iconURL); -} - -/** - * Create a UI entry for a recently closed tab or window. - * @param aTagName - * the tag name that will be used when creating the UI entry - * @param aIsWindowsFragment - * whether or not this entry will represent a closed window - * @param aIndex - * the index of the closed tab - * @param aClosedTab - * the closed tab - * @param aDocument - * a document that can be used to create the entry - * @param aMenuLabel - * the label the created entry will have - * @param aFragment - * the fragment the created entry will be in - */ -function createEntry(aTagName, aIsWindowsFragment, aIndex, aClosedTab, - aDocument, aMenuLabel, aFragment) { - let element = aDocument.createElementNS(kNSXUL, aTagName); - - element.setAttribute("label", aMenuLabel); - if (aClosedTab.image) { - setImage(aClosedTab, element); - } - if (!aIsWindowsFragment) { - element.setAttribute("value", aIndex); - } - - if (aTagName == "menuitem") { - element.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon"); - } - - element.setAttribute("oncommand", "undoClose" + (aIsWindowsFragment ? "Window" : "Tab") + - "(" + aIndex + ");"); - - // Set the targetURI attribute so it will be shown in tooltip. - // SessionStore uses one-based indexes, so we need to normalize them. - let tabData; - tabData = aIsWindowsFragment ? aClosedTab - : aClosedTab.state; - let activeIndex = (tabData.index || tabData.entries.length) - 1; - if (activeIndex >= 0 && tabData.entries[activeIndex]) { - element.setAttribute("targetURI", tabData.entries[activeIndex].url); - } - - if (!aIsWindowsFragment) { - element.addEventListener("click", RecentlyClosedTabsAndWindowsMenuUtils._undoCloseMiddleClick, false); - } - if (aIndex == 0) { - element.setAttribute("key", "key_undoClose" + (aIsWindowsFragment? "Window" : "Tab")); - } - - aFragment.appendChild(element); -} - -/** - * Create an entry to restore all closed windows or tabs. - * @param aDocument - * a document that can be used to create the entry - * @param aFragment - * the fragment the created entry will be in - * @param aPrefixRestoreAll - * whether the 'restore all windows' item is suffixed or prefixed to the list - * If suffixed a separator will be inserted before it. - * @param aIsWindowsFragment - * whether or not this entry will represent a closed window - * @param aRestoreAllLabel - * which localizable string to use for the entry - * @param aEntryCount - * the number of elements to be restored by this entry - * @param aTagName - * the tag name that will be used when creating the UI entry - */ -function createRestoreAllEntry(aDocument, aFragment, aPrefixRestoreAll, - aIsWindowsFragment, aRestoreAllLabel, - aEntryCount, aTagName) { - let restoreAllElements = aDocument.createElementNS(kNSXUL, aTagName); - restoreAllElements.classList.add("restoreallitem"); - restoreAllElements.setAttribute("label", navigatorBundle.GetStringFromName(aRestoreAllLabel)); - restoreAllElements.setAttribute("oncommand", - "for (var i = 0; i < " + aEntryCount + "; i++) undoClose" + - (aIsWindowsFragment? "Window" : "Tab") + "();"); - if (aPrefixRestoreAll) { - aFragment.insertBefore(restoreAllElements, aFragment.firstChild); - } else { - aFragment.appendChild(aDocument.createElementNS(kNSXUL, "menuseparator")); - aFragment.appendChild(restoreAllElements); - } -}
\ No newline at end of file diff --git a/browser/components/sessionstore/RunState.jsm b/browser/components/sessionstore/RunState.jsm deleted file mode 100644 index 3cdf47718..000000000 --- a/browser/components/sessionstore/RunState.jsm +++ /dev/null @@ -1,96 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["RunState"]; - -const STATE_STOPPED = 0; -const STATE_RUNNING = 1; -const STATE_QUITTING = 2; -const STATE_CLOSING = 3; -const STATE_CLOSED = 4; - -// We're initially stopped. -var state = STATE_STOPPED; - -/** - * This module keeps track of SessionStore's current run state. We will - * always start out at STATE_STOPPED. After the session was read from disk and - * the initial browser window has loaded we switch to STATE_RUNNING. On the - * first notice that a browser shutdown was granted we switch to STATE_QUITTING. - */ -this.RunState = Object.freeze({ - // If we're stopped then SessionStore hasn't been initialized yet. As soon - // as the session is read from disk and the initial browser window has loaded - // the run state will change to STATE_RUNNING. - get isStopped() { - return state == STATE_STOPPED; - }, - - // STATE_RUNNING is our default mode of operation that we'll spend most of - // the time in. After the session was read from disk and the first browser - // window has loaded we remain running until the browser quits. - get isRunning() { - return state == STATE_RUNNING; - }, - - // We will enter STATE_QUITTING as soon as we receive notice that a browser - // shutdown was granted. SessionStore will use this information to prevent - // us from collecting partial information while the browser is shutting down - // as well as to allow a last single write to disk and block all writes after - // that. - get isQuitting() { - return state >= STATE_QUITTING; - }, - - // We will enter STATE_CLOSING as soon as SessionStore is uninitialized. - // The SessionFile module will know that a last write will happen in this - // state and it can do some necessary cleanup. - get isClosing() { - return state == STATE_CLOSING; - }, - - // We will enter STATE_CLOSED as soon as SessionFile has written to disk for - // the last time before shutdown and will not accept any further writes. - get isClosed() { - return state == STATE_CLOSED; - }, - - // Switch the run state to STATE_RUNNING. This must be called after the - // session was read from, the initial browser window has loaded and we're - // now ready to restore session data. - setRunning() { - if (this.isStopped) { - state = STATE_RUNNING; - } - }, - - // Switch the run state to STATE_CLOSING. This must be called *before* the - // last SessionFile.write() call so that SessionFile knows we're closing and - // can do some last cleanups and write a proper sessionstore.js file. - setClosing() { - if (this.isQuitting) { - state = STATE_CLOSING; - } - }, - - // Switch the run state to STATE_CLOSED. This must be called by SessionFile - // after the last write to disk was accepted and no further writes will be - // allowed. Any writes after this stage will cause exceptions. - setClosed() { - if (this.isClosing) { - state = STATE_CLOSED; - } - }, - - // Switch the run state to STATE_QUITTING. This should be called once we're - // certain that the browser is going away and before we start collecting the - // final window states to save in the session file. - setQuitting() { - if (this.isRunning) { - state = STATE_QUITTING; - } - }, -}); diff --git a/browser/components/sessionstore/SessionCookies.jsm b/browser/components/sessionstore/SessionCookies.jsm deleted file mode 100644 index b99ab927b..000000000 --- a/browser/components/sessionstore/SessionCookies.jsm +++ /dev/null @@ -1,476 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["SessionCookies"]; - -const Cu = Components.utils; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/Services.jsm", this); -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); - -XPCOMUtils.defineLazyModuleGetter(this, "Utils", - "resource://gre/modules/sessionstore/Utils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", - "resource:///modules/sessionstore/PrivacyLevel.jsm"); - -// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision. -const MAX_EXPIRY = Math.pow(2, 62); - -/** - * The external API implemented by the SessionCookies module. - */ -this.SessionCookies = Object.freeze({ - update: function (windows) { - SessionCookiesInternal.update(windows); - }, - - getHostsForWindow: function (window, checkPrivacy = false) { - return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy); - }, - - restore(cookies) { - SessionCookiesInternal.restore(cookies); - } -}); - -/** - * The internal API. - */ -var SessionCookiesInternal = { - /** - * Stores whether we're initialized, yet. - */ - _initialized: false, - - /** - * Retrieve the list of all hosts contained in the given windows' session - * history entries (per window) and collect the associated cookies for those - * hosts, if any. The given state object is being modified. - * - * @param windows - * Array of window state objects. - * [{ tabs: [...], cookies: [...] }, ...] - */ - update: function (windows) { - this._ensureInitialized(); - - for (let window of windows) { - let cookies = []; - - // Collect all hosts for the current window. - let hosts = this.getHostsForWindow(window, true); - - for (let host of Object.keys(hosts)) { - let isPinned = hosts[host]; - - for (let cookie of CookieStore.getCookiesForHost(host)) { - // _getCookiesForHost() will only return hosts with the right privacy - // rules, so there is no need to do anything special with this call - // to PrivacyLevel.canSave(). - if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) { - cookies.push(cookie); - } - } - } - - // Don't include/keep empty cookie sections. - if (cookies.length) { - window.cookies = cookies; - } else if ("cookies" in window) { - delete window.cookies; - } - } - }, - - /** - * Returns a map of all hosts for a given window that we might want to - * collect cookies for. - * - * @param window - * A window state object containing tabs with history entries. - * @param checkPrivacy (bool) - * Whether to check the privacy level for each host. - * @return {object} A map of hosts for a given window state object. The keys - * will be hosts, the values are boolean and determine - * whether we will use the deferred privacy level when - * checking how much data to save on quitting. - */ - getHostsForWindow: function (window, checkPrivacy = false) { - let hosts = {}; - - for (let tab of window.tabs) { - for (let entry of tab.entries) { - this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned); - } - } - - return hosts; - }, - - /** - * Restores a given list of session cookies. - */ - restore(cookies) { - - for (let cookie of cookies) { - let expiry = "expiry" in cookie ? cookie.expiry : MAX_EXPIRY; - let cookieObj = { - host: cookie.host, - path: cookie.path || "", - name: cookie.name || "" - }; - if (!Services.cookies.cookieExists(cookieObj, cookie.originAttributes || {})) { - Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "", - cookie.value, !!cookie.secure, !!cookie.httponly, - /* isSession = */ true, expiry, cookie.originAttributes || {}); - } - } - }, - - /** - * Handles observers notifications that are sent whenever cookies are added, - * changed, or removed. Ensures that the storage is updated accordingly. - */ - observe: function (subject, topic, data) { - switch (data) { - case "added": - case "changed": - this._updateCookie(subject); - break; - case "deleted": - this._removeCookie(subject); - break; - case "cleared": - CookieStore.clear(); - break; - case "batch-deleted": - this._removeCookies(subject); - break; - case "reload": - CookieStore.clear(); - this._reloadCookies(); - break; - default: - throw new Error("Unhandled cookie-changed notification."); - } - }, - - /** - * If called for the first time in a session, iterates all cookies in the - * cookies service and puts them into the store if they're session cookies. - */ - _ensureInitialized: function () { - if (!this._initialized) { - this._reloadCookies(); - this._initialized = true; - Services.obs.addObserver(this, "cookie-changed", false); - } - }, - - /** - * Fill a given map with hosts found in the given entry's session history and - * any child entries. - * - * @param entry - * the history entry, serialized - * @param hosts - * the hash that will be used to store hosts eg, { hostname: true } - * @param checkPrivacy - * should we check the privacy level for https - * @param isPinned - * is the entry we're evaluating for a pinned tab; used only if - * checkPrivacy - */ - _extractHostsFromEntry: function (entry, hosts, checkPrivacy, isPinned) { - let host = entry._host; - let scheme = entry._scheme; - - // If host & scheme aren't defined, then we are likely here in the startup - // process via _splitCookiesFromWindow. In that case, we'll turn entry.url - // into an nsIURI and get host/scheme from that. This will throw for about: - // urls in which case we don't need to do anything. - if (!host && !scheme) { - try { - let uri = Utils.makeURI(entry.url); - host = uri.host; - scheme = uri.scheme; - this._extractHostsFromHostScheme(host, scheme, hosts, checkPrivacy, isPinned); - } - catch (ex) { } - } - - if (entry.children) { - for (let child of entry.children) { - this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned); - } - } - }, - - /** - * Add a given host to a given map of hosts if the privacy level allows - * saving cookie data for it. - * - * @param host - * the host of a uri (usually via nsIURI.host) - * @param scheme - * the scheme of a uri (usually via nsIURI.scheme) - * @param hosts - * the hash that will be used to store hosts eg, { hostname: true } - * @param checkPrivacy - * should we check the privacy level for https - * @param isPinned - * is the entry we're evaluating for a pinned tab; used only if - * checkPrivacy - */ - _extractHostsFromHostScheme: - function (host, scheme, hosts, checkPrivacy, isPinned) { - // host and scheme may not be set (for about: urls for example), in which - // case testing scheme will be sufficient. - if (/https?/.test(scheme) && !hosts[host] && - (!checkPrivacy || - PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) { - // By setting this to true or false, we can determine when looking at - // the host in update() if we should check for privacy. - hosts[host] = isPinned; - } else if (scheme == "file") { - hosts[host] = true; - } - }, - - /** - * Updates or adds a given cookie to the store. - */ - _updateCookie: function (cookie) { - cookie.QueryInterface(Ci.nsICookie2); - - if (cookie.isSession) { - CookieStore.set(cookie); - } else { - CookieStore.delete(cookie); - } - }, - - /** - * Removes a given cookie from the store. - */ - _removeCookie: function (cookie) { - cookie.QueryInterface(Ci.nsICookie2); - - if (cookie.isSession) { - CookieStore.delete(cookie); - } - }, - - /** - * Removes a given list of cookies from the store. - */ - _removeCookies: function (cookies) { - for (let i = 0; i < cookies.length; i++) { - this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2)); - } - }, - - /** - * Iterates all cookies in the cookies service and puts them into the store - * if they're session cookies. - */ - _reloadCookies: function () { - let iter = Services.cookies.enumerator; - while (iter.hasMoreElements()) { - this._updateCookie(iter.getNext()); - } - } -}; - -/** - * Generates all possible subdomains for a given host and prepends a leading - * dot to all variants. - * - * See http://tools.ietf.org/html/rfc6265#section-5.1.3 - * http://en.wikipedia.org/wiki/HTTP_cookie#Domain_and_Path - * - * All cookies belonging to a web page will be internally represented by a - * nsICookie object. nsICookie.host will be the request host if no domain - * parameter was given when setting the cookie. If a specific domain was given - * then nsICookie.host will contain that specific domain and prepend a leading - * dot to it. - * - * We thus generate all possible subdomains for a given domain and prepend a - * leading dot to them as that is the value that was used as the map key when - * the cookie was set. - */ -function* getPossibleSubdomainVariants(host) { - // Try given domain with a leading dot (.www.example.com). - yield "." + host; - - // Stop if there are only two parts left (e.g. example.com was given). - let parts = host.split("."); - if (parts.length < 3) { - return; - } - - // Remove the first subdomain (www.example.com -> example.com). - let rest = parts.slice(1).join("."); - - // Try possible parent subdomains. - yield* getPossibleSubdomainVariants(rest); -} - -/** - * The internal cookie storage that keeps track of every active session cookie. - * These are stored using maps per host, path, and cookie name. - */ -var CookieStore = { - /** - * The internal structure holding all known cookies. - * - * Host => - * Path => - * Name => {path: "/", name: "sessionid", secure: true} - * - * Maps are used for storage but the data structure is equivalent to this: - * - * this._hosts = { - * "www.mozilla.org": { - * "/": { - * "username": {name: "username", value: "my_name_is", etc...}, - * "sessionid": {name: "sessionid", value: "1fdb3a", etc...} - * } - * }, - * "tbpl.mozilla.org": { - * "/path": { - * "cookiename": {name: "cookiename", value: "value", etc...} - * } - * }, - * ".example.com": { - * "/path": { - * "cookiename": {name: "cookiename", value: "value", etc...} - * } - * } - * }; - */ - _hosts: new Map(), - - /** - * Returns the list of stored session cookies for a given host. - * - * @param host - * A string containing the host name we want to get cookies for. - */ - getCookiesForHost: function (host) { - let cookies = []; - - let appendCookiesForHost = host => { - if (!this._hosts.has(host)) { - return; - } - - for (let pathToNamesMap of this._hosts.get(host).values()) { - for (let nameToCookiesMap of pathToNamesMap.values()) { - cookies.push(...nameToCookiesMap.values()); - } - } - } - - // Try to find cookies for the given host, e.g. <www.example.com>. - // The full hostname will be in the map if the Set-Cookie header did not - // have a domain= attribute, i.e. the cookie will only be stored for the - // request domain. Also, try to find cookies for subdomains, e.g. - // <.example.com>. We will find those variants with a leading dot in the - // map if the Set-Cookie header had a domain= attribute, i.e. the cookie - // will be stored for a parent domain and we send it for any subdomain. - for (let variant of [host, ...getPossibleSubdomainVariants(host)]) { - appendCookiesForHost(variant); - } - - return cookies; - }, - - /** - * Stores a given cookie. - * - * @param cookie - * The nsICookie2 object to add to the storage. - */ - set: function (cookie) { - let jscookie = {host: cookie.host, value: cookie.value}; - - // Only add properties with non-default values to save a few bytes. - if (cookie.path) { - jscookie.path = cookie.path; - } - - if (cookie.name) { - jscookie.name = cookie.name; - } - - if (cookie.isSecure) { - jscookie.secure = true; - } - - if (cookie.isHttpOnly) { - jscookie.httponly = true; - } - - if (cookie.expiry < MAX_EXPIRY) { - jscookie.expiry = cookie.expiry; - } - - if (cookie.originAttributes) { - jscookie.originAttributes = cookie.originAttributes; - } - - this._ensureMap(cookie).set(cookie.name, jscookie); - }, - - /** - * Removes a given cookie. - * - * @param cookie - * The nsICookie2 object to be removed from storage. - */ - delete: function (cookie) { - this._ensureMap(cookie).delete(cookie.name); - }, - - /** - * Removes all cookies. - */ - clear: function () { - this._hosts.clear(); - }, - - /** - * Creates all maps necessary to store a given cookie. - * - * @param cookie - * The nsICookie2 object to create maps for. - * - * @return The newly created Map instance mapping cookie names to - * internal jscookies, in the given path of the given host. - */ - _ensureMap: function (cookie) { - if (!this._hosts.has(cookie.host)) { - this._hosts.set(cookie.host, new Map()); - } - - let originAttributesMap = this._hosts.get(cookie.host); - // If cookie.originAttributes is null, originAttributes will be an empty string. - let originAttributes = ChromeUtils.originAttributesToSuffix(cookie.originAttributes); - if (!originAttributesMap.has(originAttributes)) { - originAttributesMap.set(originAttributes, new Map()); - } - - let pathToNamesMap = originAttributesMap.get(originAttributes); - - if (!pathToNamesMap.has(cookie.path)) { - pathToNamesMap.set(cookie.path, new Map()); - } - - return pathToNamesMap.get(cookie.path); - } -}; diff --git a/browser/components/sessionstore/SessionFile.jsm b/browser/components/sessionstore/SessionFile.jsm deleted file mode 100644 index 80c4e7790..000000000 --- a/browser/components/sessionstore/SessionFile.jsm +++ /dev/null @@ -1,399 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["SessionFile"]; - -/** - * Implementation of all the disk I/O required by the session store. - * This is a private API, meant to be used only by the session store. - * It will change. Do not use it for any other purpose. - * - * Note that this module implicitly depends on one of two things: - * 1. either the asynchronous file I/O system enqueues its requests - * and never attempts to simultaneously execute two I/O requests on - * the files used by this module from two distinct threads; or - * 2. the clients of this API are well-behaved and do not place - * concurrent requests to the files used by this module. - * - * Otherwise, we could encounter bugs, especially under Windows, - * e.g. if a request attempts to write sessionstore.js while - * another attempts to copy that file. - * - * This implementation uses OS.File, which guarantees property 1. - */ - -const Cu = Components.utils; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/osfile.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/AsyncShutdown.jsm"); -Cu.import("resource://gre/modules/Preferences.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "console", - "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", - "resource://gre/modules/PromiseUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "RunState", - "resource:///modules/sessionstore/RunState.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", - "resource://gre/modules/TelemetryStopwatch.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); -XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", - "@mozilla.org/base/telemetry;1", "nsITelemetry"); -XPCOMUtils.defineLazyServiceGetter(this, "sessionStartup", - "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionWorker", - "resource:///modules/sessionstore/SessionWorker.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", - "resource:///modules/sessionstore/SessionStore.jsm"); - -const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID"; -const PREF_MAX_UPGRADE_BACKUPS = "browser.sessionstore.upgradeBackup.maxUpgradeBackups"; - -const PREF_MAX_SERIALIZE_BACK = "browser.sessionstore.max_serialize_back"; -const PREF_MAX_SERIALIZE_FWD = "browser.sessionstore.max_serialize_forward"; - -this.SessionFile = { - /** - * Read the contents of the session file, asynchronously. - */ - read: function () { - return SessionFileInternal.read(); - }, - /** - * Write the contents of the session file, asynchronously. - */ - write: function (aData) { - return SessionFileInternal.write(aData); - }, - /** - * Wipe the contents of the session file, asynchronously. - */ - wipe: function () { - return SessionFileInternal.wipe(); - }, - - /** - * Return the paths to the files used to store, backup, etc. - * the state of the file. - */ - get Paths() { - return SessionFileInternal.Paths; - } -}; - -Object.freeze(SessionFile); - -var Path = OS.Path; -var profileDir = OS.Constants.Path.profileDir; - -var SessionFileInternal = { - Paths: Object.freeze({ - // The path to the latest version of sessionstore written during a clean - // shutdown. After startup, it is renamed `cleanBackup`. - clean: Path.join(profileDir, "sessionstore.js"), - - // The path at which we store the previous version of `clean`. Updated - // whenever we successfully load from `clean`. - cleanBackup: Path.join(profileDir, "sessionstore-backups", "previous.js"), - - // The directory containing all sessionstore backups. - backups: Path.join(profileDir, "sessionstore-backups"), - - // The path to the latest version of the sessionstore written - // during runtime. Generally, this file contains more - // privacy-sensitive information than |clean|, and this file is - // therefore removed during clean shutdown. This file is designed to protect - // against crashes / sudden shutdown. - recovery: Path.join(profileDir, "sessionstore-backups", "recovery.js"), - - // The path to the previous version of the sessionstore written - // during runtime (e.g. 15 seconds before recovery). In case of a - // clean shutdown, this file is removed. Generally, this file - // contains more privacy-sensitive information than |clean|, and - // this file is therefore removed during clean shutdown. This - // file is designed to protect against crashes that are nasty - // enough to corrupt |recovery|. - recoveryBackup: Path.join(profileDir, "sessionstore-backups", "recovery.bak"), - - // The path to a backup created during an upgrade of Firefox. - // Having this backup protects the user essentially from bugs in - // Firefox or add-ons, especially for users of Nightly. This file - // does not contain any information more sensitive than |clean|. - upgradeBackupPrefix: Path.join(profileDir, "sessionstore-backups", "upgrade.js-"), - - // The path to the backup of the version of the session store used - // during the latest upgrade of Firefox. During load/recovery, - // this file should be used if both |path|, |backupPath| and - // |latestStartPath| are absent/incorrect. May be "" if no - // upgrade backup has ever been performed. This file does not - // contain any information more sensitive than |clean|. - get upgradeBackup() { - let latestBackupID = SessionFileInternal.latestUpgradeBackupID; - if (!latestBackupID) { - return ""; - } - return this.upgradeBackupPrefix + latestBackupID; - }, - - // The path to a backup created during an upgrade of Firefox. - // Having this backup protects the user essentially from bugs in - // Firefox, especially for users of Nightly. - get nextUpgradeBackup() { - return this.upgradeBackupPrefix + Services.appinfo.platformBuildID; - }, - - /** - * The order in which to search for a valid sessionstore file. - */ - get loadOrder() { - // If `clean` exists and has been written without corruption during - // the latest shutdown, we need to use it. - // - // Otherwise, `recovery` and `recoveryBackup` represent the most - // recent state of the session store. - // - // Finally, if nothing works, fall back to the last known state - // that can be loaded (`cleanBackup`) or, if available, to the - // backup performed during the latest upgrade. - let order = ["clean", - "recovery", - "recoveryBackup", - "cleanBackup"]; - if (SessionFileInternal.latestUpgradeBackupID) { - // We have an upgradeBackup - order.push("upgradeBackup"); - } - return order; - }, - }), - - // Number of attempted calls to `write`. - // Note that we may have _attempts > _successes + _failures, - // if attempts never complete. - // Used for error reporting. - _attempts: 0, - - // Number of successful calls to `write`. - // Used for error reporting. - _successes: 0, - - // Number of failed calls to `write`. - // Used for error reporting. - _failures: 0, - - // Resolved once initialization is complete. - // The promise never rejects. - _deferredInitialized: PromiseUtils.defer(), - - // `true` once we have started initialization, i.e. once something - // has been scheduled that will eventually resolve `_deferredInitialized`. - _initializationStarted: false, - - // The ID of the latest version of Gecko for which we have an upgrade backup - // or |undefined| if no upgrade backup was ever written. - get latestUpgradeBackupID() { - try { - return Services.prefs.getCharPref(PREF_UPGRADE_BACKUP); - } catch (ex) { - return undefined; - } - }, - - // Find the correct session file, read it and setup the worker. - read: Task.async(function* () { - this._initializationStarted = true; - - let result; - let noFilesFound = true; - // Attempt to load by order of priority from the various backups - for (let key of this.Paths.loadOrder) { - let corrupted = false; - let exists = true; - try { - let path = this.Paths[key]; - let startMs = Date.now(); - - let source = yield OS.File.read(path, { encoding: "utf-8" }); - let parsed = JSON.parse(source); - - if (!SessionStore.isFormatVersionCompatible(parsed.version || ["sessionrestore", 0] /*fallback for old versions*/)) { - // Skip sessionstore files that we don't understand. - Cu.reportError("Cannot extract data from Session Restore file " + path + ". Wrong format/version: " + JSON.stringify(parsed.version) + "."); - continue; - } - result = { - origin: key, - source: source, - parsed: parsed - }; - Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE"). - add(false); - Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS"). - add(Date.now() - startMs); - break; - } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { - exists = false; - } catch (ex if ex instanceof OS.File.Error) { - // The file might be inaccessible due to wrong permissions - // or similar failures. We'll just count it as "corrupted". - console.error("Could not read session file ", ex, ex.stack); - corrupted = true; - } catch (ex if ex instanceof SyntaxError) { - console.error("Corrupt session file (invalid JSON found) ", ex, ex.stack); - // File is corrupted, try next file - corrupted = true; - } finally { - if (exists) { - noFilesFound = false; - Telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE"). - add(corrupted); - } - } - } - - // All files are corrupted if files found but none could deliver a result. - let allCorrupt = !noFilesFound && !result; - Telemetry.getHistogramById("FX_SESSION_RESTORE_ALL_FILES_CORRUPT"). - add(allCorrupt); - - if (!result) { - // If everything fails, start with an empty session. - result = { - origin: "empty", - source: "", - parsed: null - }; - } - - result.noFilesFound = noFilesFound; - - // Initialize the worker (in the background) to let it handle backups and also - // as a workaround for bug 964531. - let promiseInitialized = SessionWorker.post("init", [result.origin, this.Paths, { - maxUpgradeBackups: Preferences.get(PREF_MAX_UPGRADE_BACKUPS, 3), - maxSerializeBack: Preferences.get(PREF_MAX_SERIALIZE_BACK, 10), - maxSerializeForward: Preferences.get(PREF_MAX_SERIALIZE_FWD, -1) - }]); - - promiseInitialized.catch(err => { - // Ensure that we report errors but that they do not stop us. - Promise.reject(err); - }).then(() => this._deferredInitialized.resolve()); - - return result; - }), - - // Post a message to the worker, making sure that it has been initialized - // first. - _postToWorker: Task.async(function*(...args) { - if (!this._initializationStarted) { - // Initializing the worker is somewhat complex, as proper handling of - // backups requires us to first read and check the session. Consequently, - // the only way to initialize the worker is to first call `this.read()`. - - // The call to `this.read()` causes background initialization of the worker. - // Initialization will be complete once `this._deferredInitialized.promise` - // resolves. - this.read(); - } - yield this._deferredInitialized.promise; - return SessionWorker.post(...args) - }), - - write: function (aData) { - if (RunState.isClosed) { - return Promise.reject(new Error("SessionFile is closed")); - } - - let isFinalWrite = false; - if (RunState.isClosing) { - // If shutdown has started, we will want to stop receiving - // write instructions. - isFinalWrite = true; - RunState.setClosed(); - } - - let performShutdownCleanup = isFinalWrite && - !sessionStartup.isAutomaticRestoreEnabled(); - - this._attempts++; - let options = {isFinalWrite, performShutdownCleanup}; - let promise = this._postToWorker("write", [aData, options]); - - // Wait until the write is done. - promise = promise.then(msg => { - // Record how long the write took. - this._recordTelemetry(msg.telemetry); - this._successes++; - if (msg.result.upgradeBackup) { - // We have just completed a backup-on-upgrade, store the information - // in preferences. - Services.prefs.setCharPref(PREF_UPGRADE_BACKUP, - Services.appinfo.platformBuildID); - } - }, err => { - // Catch and report any errors. - console.error("Could not write session state file ", err, err.stack); - this._failures++; - // By not doing anything special here we ensure that |promise| cannot - // be rejected anymore. The shutdown/cleanup code at the end of the - // function will thus always be executed. - }); - - // Ensure that we can write sessionstore.js cleanly before the profile - // becomes unaccessible. - AsyncShutdown.profileBeforeChange.addBlocker( - "SessionFile: Finish writing Session Restore data", - promise, - { - fetchState: () => ({ - options, - attempts: this._attempts, - successes: this._successes, - failures: this._failures, - }) - }); - - // This code will always be executed because |promise| can't fail anymore. - // We ensured that by having a reject handler that reports the failure but - // doesn't forward the rejection. - return promise.then(() => { - // Remove the blocker, no matter if writing failed or not. - AsyncShutdown.profileBeforeChange.removeBlocker(promise); - - if (isFinalWrite) { - Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", ""); - } - }); - }, - - wipe: function () { - return this._postToWorker("wipe"); - }, - - _recordTelemetry: function(telemetry) { - for (let id of Object.keys(telemetry)){ - let value = telemetry[id]; - let samples = []; - if (Array.isArray(value)) { - samples.push(...value); - } else { - samples.push(value); - } - let histogram = Telemetry.getHistogramById(id); - for (let sample of samples) { - histogram.add(sample); - } - } - } -}; diff --git a/browser/components/sessionstore/SessionHistory.jsm b/browser/components/sessionstore/SessionHistory.jsm deleted file mode 100644 index 3d28d87db..000000000 --- a/browser/components/sessionstore/SessionHistory.jsm +++ /dev/null @@ -1,431 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["SessionHistory"]; - -const Cu = Components.utils; -const Cc = Components.classes; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Utils", - "resource://gre/modules/sessionstore/Utils.jsm"); - -function debug(msg) { - Services.console.logStringMessage("SessionHistory: " + msg); -} - -/** - * The external API exported by this module. - */ -this.SessionHistory = Object.freeze({ - isEmpty: function (docShell) { - return SessionHistoryInternal.isEmpty(docShell); - }, - - collect: function (docShell) { - return SessionHistoryInternal.collect(docShell); - }, - - restore: function (docShell, tabData) { - SessionHistoryInternal.restore(docShell, tabData); - } -}); - -/** - * The internal API for the SessionHistory module. - */ -var SessionHistoryInternal = { - /** - * Returns whether the given docShell's session history is empty. - * - * @param docShell - * The docShell that owns the session history. - */ - isEmpty: function (docShell) { - let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); - let history = webNavigation.sessionHistory; - if (!webNavigation.currentURI) { - return true; - } - let uri = webNavigation.currentURI.spec; - return uri == "about:blank" && history.count == 0; - }, - - /** - * Collects session history data for a given docShell. - * - * @param docShell - * The docShell that owns the session history. - */ - collect: function (docShell) { - let loadContext = docShell.QueryInterface(Ci.nsILoadContext); - let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); - let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal); - - let data = {entries: [], userContextId: loadContext.originAttributes.userContextId }; - - if (history && history.count > 0) { - // Loop over the transaction linked list directly so we can get the - // persist property for each transaction. - for (let txn = history.rootTransaction; txn; txn = txn.next) { - let entry = this.serializeEntry(txn.sHEntry); - entry.persist = txn.persist; - data.entries.push(entry); - } - - // Ensure the index isn't out of bounds if an exception was thrown above. - data.index = Math.min(history.index + 1, data.entries.length); - } - - // 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. - if (data.entries.length == 0) { - let uri = webNavigation.currentURI.spec; - 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 - // or it's a blank tab that was modified (like a custom newtab page), - // record it. For about:blank we explicitly want an empty array without - // an 'index' property to denote that there are no history entries. - if (uri != "about:blank" || (body && body.hasChildNodes())) { - data.entries.push({ - url: uri, - triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL - }); - data.index = 1; - } - } - - return data; - }, - - /** - * Get an object that is a serialized representation of a History entry. - * - * @param shEntry - * nsISHEntry instance - * @return object - */ - serializeEntry: function (shEntry) { - let entry = { url: shEntry.URI.spec }; - - // Save some bytes and don't include the title property - // if that's identical to the current entry's URL. - if (shEntry.title && shEntry.title != entry.url) { - entry.title = shEntry.title; - } - if (shEntry.isSubFrame) { - entry.subframe = true; - } - - entry.charset = shEntry.URI.originCharset; - - let cacheKey = shEntry.cacheKey; - if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && - cacheKey.data != 0) { - // XXXbz would be better to have cache keys implement - // nsISerializable or something. - entry.cacheKey = cacheKey.data; - } - entry.ID = shEntry.ID; - entry.docshellID = shEntry.docshellID; - - // We will include the property only if it's truthy to save a couple of - // bytes when the resulting object is stringified and saved to disk. - if (shEntry.referrerURI) { - entry.referrer = shEntry.referrerURI.spec; - entry.referrerPolicy = shEntry.referrerPolicy; - } - - if (shEntry.originalURI) { - entry.originalURI = shEntry.originalURI.spec; - } - - if (shEntry.loadReplace) { - entry.loadReplace = shEntry.loadReplace; - } - - if (shEntry.srcdocData) - entry.srcdocData = shEntry.srcdocData; - - if (shEntry.isSrcdocEntry) - entry.isSrcdocEntry = shEntry.isSrcdocEntry; - - if (shEntry.baseURI) - entry.baseURI = shEntry.baseURI.spec; - - if (shEntry.contentType) - entry.contentType = shEntry.contentType; - - if (shEntry.scrollRestorationIsManual) { - entry.scrollRestorationIsManual = true; - } else { - let x = {}, y = {}; - shEntry.getScrollPosition(x, y); - if (x.value != 0 || y.value != 0) - entry.scroll = x.value + "," + y.value; - } - - // Collect triggeringPrincipal data for the current history entry. - // Please note that before Bug 1297338 there was no concept of a - // principalToInherit. To remain backward/forward compatible we - // serialize the principalToInherit as triggeringPrincipal_b64. - // Once principalToInherit is well established (within FF55) - // we can update this code, remove triggeringPrincipal_b64 and - // just keep triggeringPrincipal_base64 as well as - // principalToInherit_base64; see Bug 1301666. - if (shEntry.principalToInherit) { - try { - let principalToInherit = Utils.serializePrincipal(shEntry.principalToInherit); - if (principalToInherit) { - entry.triggeringPrincipal_b64 = principalToInherit; - entry.principalToInherit_base64 = principalToInherit; - } - } catch (e) { - debug(e); - } - } - - if (shEntry.triggeringPrincipal) { - try { - let triggeringPrincipal = Utils.serializePrincipal(shEntry.triggeringPrincipal); - if (triggeringPrincipal) { - entry.triggeringPrincipal_base64 = triggeringPrincipal; - } - } catch (e) { - debug(e); - } - } - - entry.docIdentifier = shEntry.BFCacheEntry.ID; - - if (shEntry.stateData != null) { - entry.structuredCloneState = shEntry.stateData.getDataAsBase64(); - entry.structuredCloneVersion = shEntry.stateData.formatVersion; - } - - if (!(shEntry instanceof Ci.nsISHContainer)) { - return entry; - } - - if (shEntry.childCount > 0 && !shEntry.hasDynamicallyAddedChild()) { - let children = []; - for (let i = 0; i < shEntry.childCount; i++) { - let child = shEntry.GetChildAt(i); - - if (child) { - // Don't try to restore framesets containing wyciwyg URLs. - // (cf. bug 424689 and bug 450595) - if (child.URI.schemeIs("wyciwyg")) { - children.length = 0; - break; - } - - children.push(this.serializeEntry(child)); - } - } - - if (children.length) { - entry.children = children; - } - } - - return entry; - }, - - /** - * Restores session history data for a given docShell. - * - * @param docShell - * The docShell that owns the session history. - * @param tabData - * The tabdata including all history entries. - */ - restore: function (docShell, tabData) { - let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); - let history = webNavigation.sessionHistory; - if (history.count > 0) { - history.PurgeHistory(history.count); - } - history.QueryInterface(Ci.nsISHistoryInternal); - - let idMap = { used: {} }; - let docIdentMap = {}; - for (let i = 0; i < tabData.entries.length; i++) { - let entry = tabData.entries[i]; - //XXXzpao Wallpaper patch for bug 514751 - if (!entry.url) - continue; - let persist = "persist" in entry ? entry.persist : true; - history.addEntry(this.deserializeEntry(entry, idMap, docIdentMap), persist); - } - - // Select the right history entry. - let index = tabData.index - 1; - if (index < history.count && history.index != index) { - history.getEntryAtIndex(index, true); - } - }, - - /** - * Expands serialized history data into a session-history-entry instance. - * - * @param entry - * Object containing serialized history data for a URL - * @param idMap - * Hash for ensuring unique frame IDs - * @param docIdentMap - * Hash to ensure reuse of BFCache entries - * @returns nsISHEntry - */ - deserializeEntry: function (entry, idMap, docIdentMap) { - - var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. - createInstance(Ci.nsISHEntry); - - shEntry.setURI(Utils.makeURI(entry.url, entry.charset)); - shEntry.setTitle(entry.title || entry.url); - if (entry.subframe) - shEntry.setIsSubFrame(entry.subframe || false); - shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; - if (entry.contentType) - shEntry.contentType = entry.contentType; - if (entry.referrer) { - shEntry.referrerURI = Utils.makeURI(entry.referrer); - shEntry.referrerPolicy = entry.referrerPolicy; - } - if (entry.originalURI) { - shEntry.originalURI = Utils.makeURI(entry.originalURI); - } - if (entry.loadReplace) { - shEntry.loadReplace = entry.loadReplace; - } - if (entry.isSrcdocEntry) - shEntry.srcdocData = entry.srcdocData; - if (entry.baseURI) - shEntry.baseURI = Utils.makeURI(entry.baseURI); - - if (entry.cacheKey) { - var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"]. - createInstance(Ci.nsISupportsPRUint32); - cacheKey.data = entry.cacheKey; - shEntry.cacheKey = cacheKey; - } - - if (entry.ID) { - // get a new unique ID for this frame (since the one from the last - // start might already be in use) - var id = idMap[entry.ID] || 0; - if (!id) { - for (id = Date.now(); id in idMap.used; id++); - idMap[entry.ID] = id; - idMap.used[id] = true; - } - shEntry.ID = id; - } - - if (entry.docshellID) - shEntry.docshellID = entry.docshellID; - - if (entry.structuredCloneState && entry.structuredCloneVersion) { - shEntry.stateData = - Cc["@mozilla.org/docshell/structured-clone-container;1"]. - createInstance(Ci.nsIStructuredCloneContainer); - - shEntry.stateData.initFromBase64(entry.structuredCloneState, - entry.structuredCloneVersion); - } - - if (entry.scrollRestorationIsManual) { - shEntry.scrollRestorationIsManual = true; - } else if (entry.scroll) { - var scrollPos = (entry.scroll || "0,0").split(","); - scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; - shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); - } - - let childDocIdents = {}; - if (entry.docIdentifier) { - // If we have a serialized document identifier, try to find an SHEntry - // which matches that doc identifier and adopt that SHEntry's - // BFCacheEntry. If we don't find a match, insert shEntry as the match - // for the document identifier. - let matchingEntry = docIdentMap[entry.docIdentifier]; - if (!matchingEntry) { - matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; - docIdentMap[entry.docIdentifier] = matchingEntry; - } - else { - shEntry.adoptBFCacheEntry(matchingEntry.shEntry); - childDocIdents = matchingEntry.childDocIdents; - } - } - - // The field entry.owner_b64 got renamed to entry.triggeringPricipal_b64 in - // Bug 1286472. To remain backward compatible we still have to support that - // field for a few cycles before we can remove it within Bug 1289785. - if (entry.owner_b64) { - entry.triggeringPricipal_b64 = entry.owner_b64; - delete entry.owner_b64; - } - - // Before introducing the concept of principalToInherit we only had - // a triggeringPrincipal within every entry which basically is the - // equivalent of the new principalToInherit. To avoid compatibility - // issues, we first check if the entry has entries for - // triggeringPrincipal_base64 and principalToInherit_base64. If not - // we fall back to using the principalToInherit (which is stored - // as triggeringPrincipal_b64) as the triggeringPrincipal and - // the principalToInherit. - // FF55 will remove the triggeringPrincipal_b64, see Bug 1301666. - if (entry.triggeringPrincipal_base64 || entry.principalToInherit_base64) { - if (entry.triggeringPrincipal_base64) { - shEntry.triggeringPrincipal = - Utils.deserializePrincipal(entry.triggeringPrincipal_base64); - } - if (entry.principalToInherit_base64) { - shEntry.principalToInherit = - Utils.deserializePrincipal(entry.principalToInherit_base64); - } - } else if (entry.triggeringPrincipal_b64) { - shEntry.triggeringPrincipal = Utils.deserializePrincipal(entry.triggeringPrincipal_b64); - shEntry.principalToInherit = shEntry.triggeringPrincipal; - } - - if (entry.children && shEntry instanceof Ci.nsISHContainer) { - for (var i = 0; i < entry.children.length; i++) { - //XXXzpao Wallpaper patch for bug 514751 - if (!entry.children[i].url) - continue; - - // We're getting sessionrestore.js files with a cycle in the - // doc-identifier graph, likely due to bug 698656. (That is, we have - // an entry where doc identifier A is an ancestor of doc identifier B, - // and another entry where doc identifier B is an ancestor of A.) - // - // If we were to respect these doc identifiers, we'd create a cycle in - // the SHEntries themselves, which causes the docshell to loop forever - // when it looks for the root SHEntry. - // - // So as a hack to fix this, we restrict the scope of a doc identifier - // to be a node's siblings and cousins, and pass childDocIdents, not - // aDocIdents, to _deserializeHistoryEntry. That is, we say that two - // SHEntries with the same doc identifier have the same document iff - // they have the same parent or their parents have the same document. - - shEntry.AddChild(this.deserializeEntry(entry.children[i], idMap, - childDocIdents), i); - } - } - - return shEntry; - }, - -}; diff --git a/browser/components/sessionstore/SessionMigration.jsm b/browser/components/sessionstore/SessionMigration.jsm deleted file mode 100644 index 1aa22f1a9..000000000 --- a/browser/components/sessionstore/SessionMigration.jsm +++ /dev/null @@ -1,106 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["SessionMigration"]; - -const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); -Cu.import("resource://gre/modules/Task.jsm", this); -Cu.import("resource://gre/modules/osfile.jsm", this); - -XPCOMUtils.defineLazyModuleGetter(this, "Utils", - "resource://gre/modules/sessionstore/Utils.jsm"); - -// An encoder to UTF-8. -XPCOMUtils.defineLazyGetter(this, "gEncoder", function () { - return new TextEncoder(); -}); - -// A decoder. -XPCOMUtils.defineLazyGetter(this, "gDecoder", function () { - return new TextDecoder(); -}); - -var SessionMigrationInternal = { - /** - * Convert the original session restore state into a minimal state. It will - * only contain: - * - open windows - * - with tabs - * - with history entries with only title, url, triggeringPrincipal - * - with pinned state - * - with tab group info (hidden + group id) - * - with selected tab info - * - with selected window info - * - * The complete state is then wrapped into the "about:welcomeback" page as - * form field info to be restored when restoring the state. - */ - convertState: function(aStateObj) { - let state = { - selectedWindow: aStateObj.selectedWindow, - _closedWindows: [] - }; - state.windows = aStateObj.windows.map(function(oldWin) { - var win = {extData: {}}; - win.tabs = oldWin.tabs.map(function(oldTab) { - var tab = {}; - // Keep only titles, urls and triggeringPrincipals for history entries - tab.entries = oldTab.entries.map(function(entry) { - return { url: entry.url, - triggeringPrincipal_base64: entry.triggeringPrincipal_base64, - title: entry.title }; - }); - tab.index = oldTab.index; - tab.hidden = oldTab.hidden; - tab.pinned = oldTab.pinned; - return tab; - }); - win.selected = oldWin.selected; - win._closedTabs = []; - return win; - }); - let url = "about:welcomeback"; - let formdata = {id: {sessionData: state}, url}; - let entry = { url, triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL }; - return { windows: [{ tabs: [{ entries: [ entry ], formdata}]}]}; - }, - /** - * Asynchronously read session restore state (JSON) from a path - */ - readState: function(aPath) { - return Task.spawn(function() { - let bytes = yield OS.File.read(aPath); - let text = gDecoder.decode(bytes); - let state = JSON.parse(text); - throw new Task.Result(state); - }); - }, - /** - * Asynchronously write session restore state as JSON to a path - */ - writeState: function(aPath, aState) { - let bytes = gEncoder.encode(JSON.stringify(aState)); - return OS.File.writeAtomic(aPath, bytes, {tmpPath: aPath + ".tmp"}); - } -} - -var SessionMigration = { - /** - * Migrate a limited set of session data from one path to another. - */ - migrate: function(aFromPath, aToPath) { - return Task.spawn(function() { - let inState = yield SessionMigrationInternal.readState(aFromPath); - let outState = SessionMigrationInternal.convertState(inState); - // Unfortunately, we can't use SessionStore's own SessionFile to - // write out the data because it has a dependency on the profile dir - // being known. When the migration runs, there is no guarantee that - // that's true. - yield SessionMigrationInternal.writeState(aToPath, outState); - }); - } -}; diff --git a/browser/components/sessionstore/SessionSaver.jsm b/browser/components/sessionstore/SessionSaver.jsm deleted file mode 100644 index d672f8877..000000000 --- a/browser/components/sessionstore/SessionSaver.jsm +++ /dev/null @@ -1,264 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["SessionSaver"]; - -const Cu = Components.utils; -const Cc = Components.classes; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/Timer.jsm", this); -Cu.import("resource://gre/modules/Services.jsm", this); -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); -Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); - -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "console", - "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter", - "resource:///modules/sessionstore/PrivacyFilter.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "RunState", - "resource:///modules/sessionstore/RunState.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", - "resource:///modules/sessionstore/SessionStore.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", - "resource:///modules/sessionstore/SessionFile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); - -// Minimal interval between two save operations (in milliseconds). -XPCOMUtils.defineLazyGetter(this, "gInterval", function () { - const PREF = "browser.sessionstore.interval"; - - // Observer that updates the cached value when the preference changes. - Services.prefs.addObserver(PREF, () => { - this.gInterval = Services.prefs.getIntPref(PREF); - - // Cancel any pending runs and call runDelayed() with - // zero to apply the newly configured interval. - SessionSaverInternal.cancel(); - SessionSaverInternal.runDelayed(0); - }, false); - - return Services.prefs.getIntPref(PREF); -}); - -// Notify observers about a given topic with a given subject. -function notify(subject, topic) { - Services.obs.notifyObservers(subject, topic, ""); -} - -// TelemetryStopwatch helper functions. -function stopWatch(method) { - return function (...histograms) { - for (let hist of histograms) { - TelemetryStopwatch[method]("FX_SESSION_RESTORE_" + hist); - } - }; -} - -var stopWatchStart = stopWatch("start"); -var stopWatchCancel = stopWatch("cancel"); -var stopWatchFinish = stopWatch("finish"); - -/** - * The external API implemented by the SessionSaver module. - */ -this.SessionSaver = Object.freeze({ - /** - * Immediately saves the current session to disk. - */ - run: function () { - return SessionSaverInternal.run(); - }, - - /** - * Saves the current session to disk delayed by a given amount of time. Should - * another delayed run be scheduled already, we will ignore the given delay - * and state saving may occur a little earlier. - */ - runDelayed: function () { - SessionSaverInternal.runDelayed(); - }, - - /** - * Sets the last save time to the current time. This will cause us to wait for - * at least the configured interval when runDelayed() is called next. - */ - updateLastSaveTime: function () { - SessionSaverInternal.updateLastSaveTime(); - }, - - /** - * Cancels all pending session saves. - */ - cancel: function () { - SessionSaverInternal.cancel(); - } -}); - -/** - * The internal API. - */ -var SessionSaverInternal = { - /** - * The timeout ID referencing an active timer for a delayed save. When no - * save is pending, this is null. - */ - _timeoutID: null, - - /** - * A timestamp that keeps track of when we saved the session last. We will - * this to determine the correct interval between delayed saves to not deceed - * the configured session write interval. - */ - _lastSaveTime: 0, - - /** - * Immediately saves the current session to disk. - */ - run: function () { - return this._saveState(true /* force-update all windows */); - }, - - /** - * Saves the current session to disk delayed by a given amount of time. Should - * another delayed run be scheduled already, we will ignore the given delay - * and state saving may occur a little earlier. - * - * @param delay (optional) - * The minimum delay in milliseconds to wait for until we collect and - * save the current session. - */ - runDelayed: function (delay = 2000) { - // Bail out if there's a pending run. - if (this._timeoutID) { - return; - } - - // Interval until the next disk operation is allowed. - delay = Math.max(this._lastSaveTime + gInterval - Date.now(), delay, 0); - - // Schedule a state save. - this._timeoutID = setTimeout(() => this._saveStateAsync(), delay); - }, - - /** - * Sets the last save time to the current time. This will cause us to wait for - * at least the configured interval when runDelayed() is called next. - */ - updateLastSaveTime: function () { - this._lastSaveTime = Date.now(); - }, - - /** - * Cancels all pending session saves. - */ - cancel: function () { - clearTimeout(this._timeoutID); - this._timeoutID = null; - }, - - /** - * Saves the current session state. Collects data and writes to disk. - * - * @param forceUpdateAllWindows (optional) - * Forces us to recollect data for all windows and will bypass and - * update the corresponding caches. - */ - _saveState: function (forceUpdateAllWindows = false) { - // Cancel any pending timeouts. - this.cancel(); - - if (PrivateBrowsingUtils.permanentPrivateBrowsing) { - // Don't save (or even collect) anything in permanent private - // browsing mode - - this.updateLastSaveTime(); - return Promise.resolve(); - } - - stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS"); - let state = SessionStore.getCurrentState(forceUpdateAllWindows); - PrivacyFilter.filterPrivateWindowsAndTabs(state); - - // Make sure that we keep the previous session if we started with a single - // private window and no non-private windows have been opened, yet. - if (state.deferredInitialState) { - state.windows = state.deferredInitialState.windows || []; - delete state.deferredInitialState; - } - - if (AppConstants.platform != "macosx") { - // We want to restore closed windows that are marked with _shouldRestore. - // We're doing this here because we want to control this only when saving - // the file. - while (state._closedWindows.length) { - let i = state._closedWindows.length - 1; - - if (!state._closedWindows[i]._shouldRestore) { - // We only need to go until _shouldRestore - // is falsy since we're going in reverse. - break; - } - - delete state._closedWindows[i]._shouldRestore; - state.windows.unshift(state._closedWindows.pop()); - } - } - - // Clear all cookies on clean shutdown according to user preferences - if (RunState.isClosing) { - let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") == - Services.cookies.QueryInterface(Ci.nsICookieService).ACCEPT_SESSION; - let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") && - Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"); - let restart = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); - // Don't clear cookies when restarting - if ((expireCookies || sanitizeCookies) && !restart) { - for (let window of state.windows) { - delete window.cookies; - } - } - } - - stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS"); - return this._writeState(state); - }, - - /** - * Saves the current session state. Collects data asynchronously and calls - * _saveState() to collect data again (with a cache hit rate of hopefully - * 100%) and write to disk afterwards. - */ - _saveStateAsync: function () { - // Allow scheduling delayed saves again. - this._timeoutID = null; - - // Write to disk. - this._saveState(); - }, - - /** - * Write the given state object to disk. - */ - _writeState: function (state) { - // We update the time stamp before writing so that we don't write again - // too soon, if saving is requested before the write completes. Without - // this update we may save repeatedly if actions cause a runDelayed - // before writing has completed. See Bug 902280 - this.updateLastSaveTime(); - - // Write (atomically) to a session file, using a tmp file. Once the session - // file is successfully updated, save the time stamp of the last save and - // notify the observers. - return SessionFile.write(state).then(() => { - this.updateLastSaveTime(); - notify(null, "sessionstore-state-write-complete"); - }, console.error); - }, -}; diff --git a/browser/components/sessionstore/SessionStorage.jsm b/browser/components/sessionstore/SessionStorage.jsm deleted file mode 100644 index 705139ebf..000000000 --- a/browser/components/sessionstore/SessionStorage.jsm +++ /dev/null @@ -1,173 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["SessionStorage"]; - -const Cu = Components.utils; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "console", - "resource://gre/modules/Console.jsm"); - -// Returns the principal for a given |frame| contained in a given |docShell|. -function getPrincipalForFrame(docShell, frame) { - let ssm = Services.scriptSecurityManager; - let uri = frame.document.documentURIObject; - return ssm.getDocShellCodebasePrincipal(uri, docShell); -} - -this.SessionStorage = Object.freeze({ - /** - * Updates all sessionStorage "super cookies" - * @param docShell - * That tab's docshell (containing the sessionStorage) - * @param frameTree - * The docShell's FrameTree instance. - * @return Returns a nested object that will have hosts as keys and per-host - * session storage data as strings. For example: - * {"example.com": {"key": "value", "my_number": "123"}} - */ - collect: function (docShell, frameTree) { - return SessionStorageInternal.collect(docShell, frameTree); - }, - - /** - * Restores all sessionStorage "super cookies". - * @param aDocShell - * A tab's docshell (containing the sessionStorage) - * @param aStorageData - * A nested object with storage data to be restored that has hosts as - * keys and per-host session storage data as strings. For example: - * {"example.com": {"key": "value", "my_number": "123"}} - */ - restore: function (aDocShell, aStorageData) { - SessionStorageInternal.restore(aDocShell, aStorageData); - }, -}); - -var SessionStorageInternal = { - /** - * Reads all session storage data from the given docShell. - * @param docShell - * A tab's docshell (containing the sessionStorage) - * @param frameTree - * The docShell's FrameTree instance. - * @return Returns a nested object that will have hosts as keys and per-host - * session storage data as strings. For example: - * {"example.com": {"key": "value", "my_number": "123"}} - */ - collect: function (docShell, frameTree) { - let data = {}; - let visitedOrigins = new Set(); - - frameTree.forEach(frame => { - let principal = getPrincipalForFrame(docShell, frame); - if (!principal) { - return; - } - - // Get the origin of the current history entry - // and use that as a key for the per-principal storage data. - let origin = principal.origin; - if (visitedOrigins.has(origin)) { - // Don't read a host twice. - return; - } - - // Mark the current origin as visited. - visitedOrigins.add(origin); - - let originData = this._readEntry(principal, docShell); - if (Object.keys(originData).length) { - data[origin] = originData; - } - }); - - return Object.keys(data).length ? data : null; - }, - - /** - * Writes session storage data to the given tab. - * @param aDocShell - * A tab's docshell (containing the sessionStorage) - * @param aStorageData - * A nested object with storage data to be restored that has hosts as - * keys and per-host session storage data as strings. For example: - * {"example.com": {"key": "value", "my_number": "123"}} - */ - restore: function (aDocShell, aStorageData) { - for (let origin of Object.keys(aStorageData)) { - let data = aStorageData[origin]; - - let principal; - - try { - let attrs = aDocShell.getOriginAttributes(); - let originURI = Services.io.newURI(origin, null, null); - principal = Services.scriptSecurityManager.createCodebasePrincipal(originURI, attrs); - } catch (e) { - console.error(e); - continue; - } - - let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager); - let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); - - // There is no need to pass documentURI, it's only used to fill documentURI property of - // domstorage event, which in this case has no consumer. Prevention of events in case - // of missing documentURI will be solved in a followup bug to bug 600307. - let storage = storageManager.createStorage(window, principal, "", aDocShell.usePrivateBrowsing); - - for (let key of Object.keys(data)) { - try { - storage.setItem(key, data[key]); - } catch (e) { - // throws e.g. for URIs that can't have sessionStorage - console.error(e); - } - } - } - }, - - /** - * Reads an entry in the session storage data contained in a tab's history. - * @param aURI - * That history entry uri - * @param aDocShell - * A tab's docshell (containing the sessionStorage) - */ - _readEntry: function (aPrincipal, aDocShell) { - let hostData = {}; - let storage; - - let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); - - try { - let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager); - storage = storageManager.getStorage(window, aPrincipal); - storage.length; // XXX: Bug 1232955 - storage.length can throw, catch that failure - } catch (e) { - // sessionStorage might throw if it's turned off, see bug 458954 - storage = null; - } - - if (storage && storage.length) { - for (let i = 0; i < storage.length; i++) { - try { - let key = storage.key(i); - hostData[key] = storage.getItem(key); - } catch (e) { - // This currently throws for secured items (cf. bug 442048). - } - } - } - - return hostData; - } -}; diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm deleted file mode 100644 index 6b30943f3..000000000 --- a/browser/components/sessionstore/SessionStore.jsm +++ /dev/null @@ -1,4746 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["SessionStore"]; - -const Cu = Components.utils; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; - -// Current version of the format used by Session Restore. -const FORMAT_VERSION = 1; - -const TAB_STATE_NEEDS_RESTORE = 1; -const TAB_STATE_RESTORING = 2; -const TAB_STATE_WILL_RESTORE = 3; - -// 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_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only - -// 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-domain-data", - "idle-daily", -]; - -// 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", - - // A crashed tab was revived by navigating to a different page. Remove its - // browser from the list of crashed browsers to stop ignoring its messages. - "SessionStore:crashedTabRevived", - - // 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:crashedTabRevived", - - // 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:crashedTabRevived", - - // 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:crashedTabRevived", - - // 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" -]; - -const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - -Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this); -Cu.import("resource://gre/modules/Promise.jsm", this); -Cu.import("resource://gre/modules/Services.jsm", this); -Cu.import("resource://gre/modules/Task.jsm", this); -Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); -Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this); -Cu.import("resource://gre/modules/Timer.jsm", this); -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); -Cu.import("resource://gre/modules/debug.js", this); -Cu.import("resource://gre/modules/osfile.jsm", this); - -XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup", - "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup"); -XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", - "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"); -XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", - "@mozilla.org/base/telemetry;1", "nsITelemetry"); -XPCOMUtils.defineLazyModuleGetter(this, "console", - "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", - "resource:///modules/RecentWindow.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "GlobalState", - "resource:///modules/sessionstore/GlobalState.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter", - "resource:///modules/sessionstore/PrivacyFilter.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "RunState", - "resource:///modules/sessionstore/RunState.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager", - "resource://devtools/client/scratchpad/scratchpad-manager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver", - "resource:///modules/sessionstore/SessionSaver.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies", - "resource:///modules/sessionstore/SessionCookies.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", - "resource:///modules/sessionstore/SessionFile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes", - "resource:///modules/sessionstore/TabAttributes.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler", - "resource:///modules/ContentCrashHandlers.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "TabState", - "resource:///modules/sessionstore/TabState.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache", - "resource:///modules/sessionstore/TabStateCache.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "TabStateFlusher", - "resource:///modules/sessionstore/TabStateFlusher.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Utils", - "resource://gre/modules/sessionstore/Utils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ViewSourceBrowser", - "resource://gre/modules/ViewSourceBrowser.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown", - "resource://gre/modules/AsyncShutdown.jsm"); - -Object.defineProperty(this, "HUDService", { - get: function HUDService_getter() { - let devtools = Cu.import("resource://devtools/shared/Loader.jsm", {}).devtools; - return devtools.require("devtools/client/webconsole/hudservice").HUDService; - }, - configurable: true, - enumerable: true -}); - -/** - * |true| if we are in debug mode, |false| otherwise. - * Debug mode is controlled by preference browser.sessionstore.debug - */ -var gDebuggingEnabled = false; -function debug(aMsg) { - if (gDebuggingEnabled) { - aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); - Services.console.logStringMessage(aMsg); - } -} - -this.SessionStore = { - get promiseInitialized() { - return SessionStoreInternal.promiseInitialized; - }, - - get canRestoreLastSession() { - return SessionStoreInternal.canRestoreLastSession; - }, - - set canRestoreLastSession(val) { - SessionStoreInternal.canRestoreLastSession = val; - }, - - get lastClosedObjectType() { - return SessionStoreInternal.lastClosedObjectType; - }, - - 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); - }, - - duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) { - return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta); - }, - - 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); - }, - - getWindowValue: function ss_getWindowValue(aWindow, aKey) { - return SessionStoreInternal.getWindowValue(aWindow, aKey); - }, - - setWindowValue: function ss_setWindowValue(aWindow, aKey, aStringValue) { - SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue); - }, - - deleteWindowValue: function ss_deleteWindowValue(aWindow, aKey) { - SessionStoreInternal.deleteWindowValue(aWindow, aKey); - }, - - getTabValue: function ss_getTabValue(aTab, aKey) { - return SessionStoreInternal.getTabValue(aTab, aKey); - }, - - setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) { - SessionStoreInternal.setTabValue(aTab, aKey, aStringValue); - }, - - deleteTabValue: function ss_deleteTabValue(aTab, aKey) { - SessionStoreInternal.deleteTabValue(aTab, aKey); - }, - - getGlobalValue: function ss_getGlobalValue(aKey) { - return SessionStoreInternal.getGlobalValue(aKey); - }, - - setGlobalValue: function ss_setGlobalValue(aKey, aStringValue) { - SessionStoreInternal.setGlobalValue(aKey, aStringValue); - }, - - deleteGlobalValue: function ss_deleteGlobalValue(aKey) { - SessionStoreInternal.deleteGlobalValue(aKey); - }, - - persistTabAttribute: function ss_persistTabAttribute(aName) { - SessionStoreInternal.persistTabAttribute(aName); - }, - - restoreLastSession: function ss_restoreLastSession() { - SessionStoreInternal.restoreLastSession(); - }, - - getCurrentState: function (aUpdateAll) { - return SessionStoreInternal.getCurrentState(aUpdateAll); - }, - - reviveCrashedTab(aTab) { - return SessionStoreInternal.reviveCrashedTab(aTab); - }, - - reviveAllCrashedTabs() { - return SessionStoreInternal.reviveAllCrashedTabs(); - }, - - navigateAndRestore(tab, loadArguments, historyIndex) { - return SessionStoreInternal.navigateAndRestore(tab, loadArguments, historyIndex); - }, - - getSessionHistory(tab, updatedCallback) { - return SessionStoreInternal.getSessionHistory(tab, updatedCallback); - }, - - undoCloseById(aClosedId) { - return SessionStoreInternal.undoCloseById(aClosedId); - }, - - /** - * 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; - }, -}; - -// Freeze the SessionStore object. We don't want anyone to modify it. -Object.freeze(SessionStore); - -var SessionStoreInternal = { - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIDOMEventListener, - Ci.nsIObserver, - Ci.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 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 -> nsIFrameLoader) 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(), - - // A map (xul:browser -> object) that maps a browser that is switching - // remoteness via navigateAndRestore, to the loadArguments that were - // most recently passed when calling navigateAndRestore. - _remotenessChangingBrowsers: new WeakMap(), - - // 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, - - // 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, - - // 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, - - // 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 = []; - let windowsEnum = Services.wm.getEnumerator("navigator:browser"); - while (windowsEnum.hasMoreElements()) { - let window = windowsEnum.getNext(); - 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"; - }, - - /** - * Initialize the sessionstore service. - */ - init: function () { - 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; - }, - - /** - * Initialize the session using the state provided by SessionStartup - */ - initSession: function () { - TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS"); - let state; - let ss = gSessionStartup; - - if (ss.doRestore() || - ss.sessionType == Ci.nsISessionStartup.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 == Ci.nsISessionStartup.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) - state = iniState; - else - state = null; - - if (remainingState.windows.length) { - LastSession.setState(remainingState); - } - } - else { - // Get the last deferred session in case the user still wants to - // restore it - LastSession.setState(state.lastSessionState); - - if (ss.previousSessionCrashed) { - this._recentCrashes = (state.session && - state.session.recentCrashes || 0) + 1; - - 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: Utils.SERIALIZED_SYSTEMPRINCIPAL }; - state = { windows: [{ tabs: [{ entries: [entry], formdata }] }] }; - } else if (this._hasSingleTabWithURL(state.windows, - "about:welcomeback")) { - // 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 = Utils.SERIALIZED_SYSTEMPRINCIPAL; - } - } - - // 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) { debug("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 : function() { - this._prefBranch = Services.prefs.getBranch("browser."); - - gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); - - Services.prefs.addObserver("browser.sessionstore.debug", () => { - gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); - }, false); - - 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); - }, - - /** - * 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); - 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(); - break; - case "browser:purge-domain-data": - this.onPurgeDomainData(aData); - break; - case "nsPref:changed": // catch pref changes - this.onPrefChange(aData); - break; - case "idle-daily": - this.onIdleDaily(); - break; - } - }, - - /** - * 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; - } - - if (aMessage.data.isFinal) { - // 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); - } 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 - // consumer that's waiting for the flush to be done. - TabStateFlusher.resolve(browser, aMessage.data.flushID); - } - - // Ignore messages from <browser> elements that have crashed - // and not yet been revived. - if (this._crashedBrowsers.has(browser.permanentKey)) { - return; - } - - // Record telemetry measurements done in the child and update the tab's - // cached state. Mark the window as dirty and trigger a delayed write. - this.recordTelemetry(aMessage.data.telemetry); - TabState.update(browser, aMessage.data); - this.saveStateDelayed(win); - - // 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. - if (this._closedTabs.has(browser.permanentKey)) { - let {closedTabs, tabData} = this._closedTabs.get(browser.permanentKey); - - // 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, tabData.state); - - // Is this the tab's final message? - if (aMessage.data.isFinal) { - // We expect no further updates. - this._closedTabs.delete(browser.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); - } - } - } - break; - case "SessionStore:restoreHistoryComplete": - // Notify the tabbrowser that the tab chrome has been restored. - let tabData = TabState.collect(tab); - - // wall-paper fix for bug 439675: make sure that the URL to be loaded - // is always visible in the address bar if no other value is present - let activePageData = tabData.entries[tabData.index - 1] || null; - let uri = activePageData ? activePageData.url || null : null; - // NB: we won't set initial URIs (about:home, about:newtab, etc.) here - // because their load will not normally trigger a location bar clearing - // when they finish loading (to avoid race conditions where we then - // clear user input instead), so we shouldn't set them here either. - // They also don't fall under the issues in bug 439675 where user input - // needs to be preserved if the load doesn't succeed. - // We also don't do this for remoteness updates, where it should not - // be necessary. - if (!browser.userTypedValue && uri && !data.isRemotenessUpdate && - !win.gInitialPages.includes(uri)) { - browser.userTypedValue = uri; - } - - // If the page has a title, set it. - if (activePageData) { - if (activePageData.title) { - tab.label = activePageData.title; - tab.crop = "end"; - } else if (activePageData.url != "about:blank") { - tab.label = activePageData.url; - tab.crop = "center"; - } - } else if (tab.hasAttribute("customizemode")) { - win.gCustomizeMode.setTab(tab); - } - - // Restore the tab icon. - if ("image" in tabData) { - // Use the serialized contentPrincipal with the new icon load. - let loadingPrincipal = Utils.deserializePrincipal(tabData.iconLoadingPrincipal); - win.gBrowser.setIcon(tab, tabData.image, loadingPrincipal); - TabStateCache.update(browser, { image: null, iconLoadingPrincipal: null }); - } - - let event = win.document.createEvent("Events"); - event.initEvent("SSTabRestoring", true, false); - tab.dispatchEvent(event); - break; - case "SessionStore:restoreTabContentStarted": - if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { - // If a load not initiated by sessionstore was started in a - // previously pending tab. Mark the tab as no longer pending. - this.markTabAsRestoring(tab); - } else if (!data.isRemotenessUpdate) { - // If the user was typing into the URL bar when we crashed, but hadn't hit - // enter yet, then we just need to write that value to the URL bar without - // loading anything. This must happen after the load, as the load will clear - // userTypedValue. - let tabData = TabState.collect(tab); - if (tabData.userTypedValue && !tabData.userTypedClear && !browser.userTypedValue) { - browser.userTypedValue = tabData.userTypedValue; - win.URLBarSetURI(); - } - - // Remove state we don't need any longer. - TabStateCache.update(browser, { - userTypedValue: null, userTypedClear: null - }); - } - break; - case "SessionStore:restoreTabContentComplete": - // This callback is used exclusively by tests that want to - // monitor the progress of network loads. - if (gDebuggingEnabled) { - Services.obs.notifyObservers(browser, NOTIFY_TAB_RESTORED, null); - } - - SessionStoreInternal._resetLocalTabRestoringState(tab); - SessionStoreInternal.restoreNextTab(); - - this._sendTabRestoredNotification(tab, data.isRemotenessUpdate); - break; - case "SessionStore:crashedTabRevived": - // The browser was revived by navigating to a different page - // manually, so we remove it from the ignored browser set. - this._crashedBrowsers.delete(browser.permanentKey); - break; - case "SessionStore:error": - this.reportInternalError(data); - TabStateFlusher.resolveAll(browser, false, "Received error from the content process"); - break; - default: - throw new Error(`received unknown message '${aMessage.name}'`); - break; - } - }, - - /** - * Record telemetry measurements stored in an object. - * @param telemetry - * {histogramID: value, ...} An object mapping histogramIDs to the - * value to be recorded for that ID, - */ - recordTelemetry: function (telemetry) { - for (let histogramId in telemetry){ - Telemetry.getHistogramById(histogramId).add(telemetry[histogramId]); - } - }, - - /* ........ Window Event Handlers .............. */ - - /** - * Implement nsIDOMEventListener 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); - 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": - this.onBrowserCrashed(target); - break; - case "XULFrameLoaderCreated": - if (target.namespaceURI == NS_XUL && - 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. - mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", 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 = gSessionStartup.state; - - // Nothing to restore now, notify observers things are complete. - Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); - } else { - TelemetryTimestamps.add("sessionRestoreRestoring"); - this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0; - - // global data must be restored before restoreWindow is called so that - // it happens before observers are notified - this._globalState.setFromState(aInitialState); - - let overwrite = this._isCmdLineEmpty(aWindow, aInitialState); - let options = {firstWindow: true, overwriteTabs: overwrite}; - this.restoreWindows(aWindow, aInitialState, options); - } - } - else { - // Nothing to restore, notify observers things are complete. - Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); - } - } - // this window was opened by _openWindowWithState - else if (!this._isWindowLoaded(aWindow)) { - let state = this._statesToRestore[aWindow.__SS_restoreID]; - let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1}; - this.restoreWindow(aWindow, state.windows[0], options); - } - // The user opened another, non-private window after starting up with - // a single private one. Let's restore the session we actually wanted to - // restore at startup. - else if (this._deferredInitialState && !isPrivateWindow && - aWindow.toolbar.visible) { - - // global data must be restored before restoreWindow is called so that - // it happens before observers are notified - this._globalState.setFromState(this._deferredInitialState); - - this._restoreCount = this._deferredInitialState.windows ? - this._deferredInitialState.windows.length : 0; - this.restoreWindows(aWindow, this._deferredInitialState, {firstWindow: true}); - this._deferredInitialState = null; - } - else if (this._restoreLastWindow && aWindow.toolbar.visible && - this._closedWindows.length && !isPrivateWindow) { - - // default to the most-recently closed window - // don't use popup windows - let closedWindowState = null; - let closedWindowIndex; - for (let i = 0; i < this._closedWindows.length; i++) { - // Take the first non-popup, point our object at it, and break out. - if (!this._closedWindows[i].isPopup) { - closedWindowState = this._closedWindows[i]; - closedWindowIndex = i; - break; - } - } - - if (closedWindowState) { - let newWindowState; - if (AppConstants.platform == "macosx" || !this._doResumeSession()) { - // We want to split the window up into pinned tabs and unpinned tabs. - // Pinned tabs should be restored. If there are any remaining tabs, - // they should be added back to _closedWindows. - // We'll cheat a little bit and reuse _prepDataForDeferredRestore - // even though it wasn't built exactly for this. - let [appTabsState, normalTabsState] = - this._prepDataForDeferredRestore({ windows: [closedWindowState] }); - - // These are our pinned tabs, which we should restore - if (appTabsState.windows.length) { - newWindowState = appTabsState.windows[0]; - delete newWindowState.__lastSessionWindowID; - } - - // In case there were no unpinned tabs, remove the window from _closedWindows - if (!normalTabsState.windows.length) { - this._closedWindows.splice(closedWindowIndex, 1); - } - // Or update _closedWindows with the modified state - else { - delete normalTabsState.windows[0].__lastSessionWindowID; - this._closedWindows[closedWindowIndex] = normalTabsState.windows[0]; - } - } - else { - // If we're just restoring the window, make sure it gets removed from - // _closedWindows. - this._closedWindows.splice(closedWindowIndex, 1); - newWindowState = closedWindowState; - delete newWindowState.hidden; - } - - if (newWindowState) { - // Ensure that the window state isn't hidden - this._restoreCount = 1; - let state = { windows: [newWindowState] }; - let options = {overwriteTabs: this._isCmdLineEmpty(aWindow, state)}; - this.restoreWindow(aWindow, newWindowState, options); - } - } - // we actually restored the session just now. - this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); - } - if (this._restoreLastWindow && aWindow.toolbar.visible) { - // always reset (if not a popup window) - // we don't want to restore a window directly after, for example, - // undoCloseWindow was executed. - this._restoreLastWindow = false; - } - }, - - /** - * Called right before a new browser window is shown. - * @param aWindow - * Window reference - */ - onBeforeBrowserWindowShown: function (aWindow) { - // Register the window. - this.onLoad(aWindow); - - // Just call initializeWindow() directly if we're initialized already. - if (this._sessionInitialized) { - this.initializeWindow(aWindow); - return; - } - - // The very first window that is opened creates a promise that is then - // re-used by all subsequent windows. The promise will be used to tell - // when we're ready for initialization. - if (!this._promiseReadyForInitialization) { - // Wait for the given window's delayed startup to be finished. - let promise = new Promise(resolve => { - Services.obs.addObserver(function obs(subject, topic) { - if (aWindow == subject) { - Services.obs.removeObserver(obs, topic); - resolve(); - } - }, "browser-delayed-startup-finished", false); - }); - - // We are ready for initialization as soon as the session file has been - // read from disk and the initial window's delayed startup has finished. - this._promiseReadyForInitialization = - Promise.all([promise, gSessionStartup.onceInitialized]); - } - - // We can't call this.onLoad since initialization - // hasn't completed, so we'll wait until it is done. - // Even if additional windows are opened and wait - // for initialization as well, the first opened - // window should execute first, and this.onLoad - // will be called with the initialState. - this._promiseReadyForInitialization.then(() => { - if (aWindow.closed) { - return; - } - - if (this._sessionInitialized) { - this.initializeWindow(aWindow); - } else { - let initialState = this.initSession(); - this._sessionInitialized = true; - - if (initialState) { - Services.obs.notifyObservers(null, NOTIFY_RESTORING_ON_STARTUP, ""); - } - TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS"); - this.initializeWindow(aWindow, initialState); - TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS"); - - // Let everyone know we're done. - this._deferredInitialized.resolve(); - } - }, console.error); - }, - - /** - * On window close... - * - remove event listeners from tabs - * - save all window data - * @param aWindow - * Window reference - */ - onClose: function ssi_onClose(aWindow) { - // this window was about to be restored - conserve its original data, if any - let isFullyLoaded = this._isWindowLoaded(aWindow); - if (!isFullyLoaded) { - if (!aWindow.__SSi) { - aWindow.__SSi = this._generateWindowID(); - } - - this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID]; - delete this._statesToRestore[aWindow.__SS_restoreID]; - delete aWindow.__SS_restoreID; - } - - // ignore windows not tracked by SessionStore - if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) { - return; - } - - // notify that the session store will stop tracking this window so that - // extensions can store any data about this window in session store before - // that's not possible anymore - let event = aWindow.document.createEvent("Events"); - event.initEvent("SSWindowClosing", true, false); - aWindow.dispatchEvent(event); - - if (this.windowToFocus && this.windowToFocus == aWindow) { - delete this.windowToFocus; - } - - var tabbrowser = aWindow.gBrowser; - - let browsers = Array.from(tabbrowser.browsers); - - TAB_EVENTS.forEach(function(aEvent) { - tabbrowser.tabContainer.removeEventListener(aEvent, this, true); - }, this); - - aWindow.gBrowser.removeEventListener("XULFrameLoaderCreated", this); - - let winData = this._windows[aWindow.__SSi]; - - // Collect window data only when *not* closed during shutdown. - if (RunState.isRunning) { - // Grab the most recent window data. The tab data will be updated - // once we finish flushing all of the messages from the tabs. - let tabMap = this._collectWindowData(aWindow); - - for (let [tab, tabData] of tabMap) { - let permanentKey = tab.linkedBrowser.permanentKey; - this._closedWindowTabs.set(permanentKey, tabData); - } - - if (isFullyLoaded) { - winData.title = tabbrowser.selectedBrowser.contentTitle || tabbrowser.selectedTab.label; - winData.title = this._replaceLoadingTitle(winData.title, tabbrowser, - tabbrowser.selectedTab); - SessionCookies.update([winData]); - } - - if (AppConstants.platform != "macosx") { - // Until we decide otherwise elsewhere, this window is part of a series - // of closing windows to quit. - winData._shouldRestore = true; - } - - // Store the window's close date to figure out when each individual tab - // was closed. This timestamp should allow re-arranging data based on how - // recently something was closed. - winData.closedAt = Date.now(); - - // we don't want to save the busy state - delete winData.busy; - - // When closing windows one after the other until Firefox quits, we - // will move those closed in series back to the "open windows" bucket - // before writing to disk. If however there is only a single window - // with tabs we deem not worth saving then we might end up with a - // random closed or even a pop-up window re-opened. To prevent that - // we explicitly allow saving an "empty" window state. - let isLastWindow = - Object.keys(this._windows).length == 1 && - !this._closedWindows.some(win => win._shouldRestore || false); - - // clear this window from the list, since it has definitely been closed. - delete this._windows[aWindow.__SSi]; - - // This window has the potential to be saved in the _closedWindows - // array (maybeSaveClosedWindows gets the final call on that). - this._saveableClosedWindowData.add(winData); - - // Now we have to figure out if this window is worth saving in the _closedWindows - // Object. - // - // We're about to flush the tabs from this window, but it's possible that we - // might never hear back from the content process(es) in time before the user - // chooses to restore the closed window. So we do the following: - // - // 1) Use the tab state cache to determine synchronously if the window is - // worth stashing in _closedWindows. - // 2) Flush the window. - // 3) When the flush is complete, revisit our decision to store the window - // in _closedWindows, and add/remove as necessary. - if (!winData.isPrivate) { - // Remove any open private tabs the window may contain. - PrivacyFilter.filterPrivateTabs(winData); - this.maybeSaveClosedWindow(winData, isLastWindow); - } - - TabStateFlusher.flushWindow(aWindow).then(() => { - // At this point, aWindow is closed! You should probably not try to - // access any DOM elements from aWindow within this callback unless - // you're holding on to them in the closure. - - for (let browser of browsers) { - if (this._closedWindowTabs.has(browser.permanentKey)) { - let tabData = this._closedWindowTabs.get(browser.permanentKey); - TabState.copyFromCache(browser, tabData); - this._closedWindowTabs.delete(browser.permanentKey); - } - } - - // Save non-private windows if they have at - // least one saveable tab or are the last window. - if (!winData.isPrivate) { - // It's possible that a tab switched its privacy state at some point - // before our flush, so we need to filter again. - PrivacyFilter.filterPrivateTabs(winData); - this.maybeSaveClosedWindow(winData, isLastWindow); - } - - // Update the tabs data now that we've got the most - // recent information. - this.cleanUpWindow(aWindow, winData, browsers); - - // save the state without this window to disk - this.saveStateDelayed(); - }); - } else { - this.cleanUpWindow(aWindow, winData, browsers); - } - - for (let i = 0; i < tabbrowser.tabs.length; i++) { - this.onTabRemove(aWindow, tabbrowser.tabs[i], true); - } - }, - - /** - * Clean up the message listeners on a window that has finally - * gone away. Call this once you're sure you don't want to hear - * from any of this windows tabs from here forward. - * - * @param aWindow - * The browser window we're cleaning up. - * @param winData - * The data for the window that we should hold in the - * DyingWindowCache in case anybody is still holding a - * reference to it. - */ - cleanUpWindow(aWindow, winData, browsers) { - // Any leftover TabStateFlusher Promises need to be resolved now, - // since we're about to remove the message listeners. - for (let browser of browsers) { - TabStateFlusher.resolveAll(browser); - } - - // Cache the window state until it is completely gone. - DyingWindowCache.set(aWindow, winData); - - let mm = aWindow.getGroupMessageManager("browsers"); - MESSAGES.forEach(msg => mm.removeMessageListener(msg, this)); - - this._saveableClosedWindowData.delete(winData); - delete aWindow.__SSi; - }, - - /** - * Decides whether or not a closed window should be put into the - * _closedWindows Object. This might be called multiple times per - * window, and will do the right thing of moving the window data - * in or out of _closedWindows if the winData indicates that our - * need for saving it has changed. - * - * @param winData - * The data for the closed window that we might save. - * @param isLastWindow - * Whether or not the window being closed is the last - * browser window. Callers of this function should pass - * in the value of SessionStoreInternal.atLastWindow for - * this argument, and pass in the same value if they happen - * to call this method again asynchronously (for example, after - * a window flush). - */ - maybeSaveClosedWindow(winData, isLastWindow) { - // Make sure SessionStore is still running, and make sure that we - // haven't chosen to forget this window. - if (RunState.isRunning && this._saveableClosedWindowData.has(winData)) { - // Determine whether the window has any tabs worth saving. - let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState); - - // Note that we might already have this window stored in - // _closedWindows from a previous call to this function. - let winIndex = this._closedWindows.indexOf(winData); - let alreadyStored = (winIndex != -1); - let shouldStore = (hasSaveableTabs || isLastWindow); - - if (shouldStore && !alreadyStored) { - let index = this._closedWindows.findIndex(win => { - return win.closedAt < winData.closedAt; - }); - - // If we found no tab closed before our - // tab then just append it to the list. - if (index == -1) { - index = this._closedWindows.length; - } - - // About to save the closed window, add a unique ID. - winData.closedId = this._nextClosedId++; - - // Insert tabData at the right position. - this._closedWindows.splice(index, 0, winData); - this._capClosedWindows(); - } else if (!shouldStore && alreadyStored) { - this._closedWindows.splice(winIndex, 1); - } - } - }, - - /** - * On quit application granted - */ - onQuitApplicationGranted: function ssi_onQuitApplicationGranted(syncShutdown=false) { - // Collect an initial snapshot of window data before we do the flush - this._forEachBrowserWindow((win) => { - this._collectWindowData(win); - }); - - // Now add an AsyncShutdown blocker that'll spin the event loop - // until the windows have all been flushed. - - // This progress object will track the state of async window flushing - // and will help us debug things that go wrong with our AsyncShutdown - // blocker. - let progress = { total: -1, current: -1 }; - - // We're going down! Switch state so that we treat closing windows and - // tabs correctly. - RunState.setQuitting(); - - if (!syncShutdown) { - // We've got some time to shut down, so let's do this properly. - // To prevent blocker from breaking the 60 sec limit(which will cause a - // crash) of async shutdown during flushing all windows, we resolve the - // promise passed to blocker once: - // 1. the flushing exceed 50 sec, or - // 2. 'oop-frameloader-crashed' or 'ipc:content-shutdown' is observed. - // Thus, Firefox still can open the last session on next startup. - AsyncShutdown.quitApplicationGranted.addBlocker( - "SessionStore: flushing all windows", - () => { - var promises = []; - promises.push(this.flushAllWindowsAsync(progress)); - promises.push(this.looseTimer(50000)); - - var promiseOFC = new Promise(resolve => { - Services.obs.addObserver(function obs(subject, topic) { - Services.obs.removeObserver(obs, topic); - resolve(); - }, "oop-frameloader-crashed", false); - }); - promises.push(promiseOFC); - - var promiseICS = new Promise(resolve => { - Services.obs.addObserver(function obs(subject, topic) { - Services.obs.removeObserver(obs, topic); - resolve(); - }, "ipc:content-shutdown", false); - }); - promises.push(promiseICS); - - return Promise.race(promises); - }, - () => progress); - } else { - // We have to shut down NOW, which means we only get to save whatever - // we already had cached. - } - }, - - /** - * An async Task that iterates all open browser windows and flushes - * any outstanding messages from their tabs. This will also close - * all of the currently open windows while we wait for the flushes - * to complete. - * - * @param progress (Object) - * Optional progress object that will be updated as async - * window flushing progresses. flushAllWindowsSync will - * write to the following properties: - * - * total (int): - * The total number of windows to be flushed. - * current (int): - * The current window that we're waiting for a flush on. - * - * @return Promise - */ - flushAllWindowsAsync: Task.async(function*(progress={}) { - let windowPromises = new Map(); - // We collect flush promises and close each window immediately so that - // the user can't start changing any window state while we're waiting - // for the flushes to finish. - this._forEachBrowserWindow((win) => { - windowPromises.set(win, TabStateFlusher.flushWindow(win)); - - // We have to wait for these messages to come up from - // each window and each browser. In the meantime, hide - // the windows to improve perceived shutdown speed. - let baseWin = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIDocShellTreeItem) - .treeOwner - .QueryInterface(Ci.nsIBaseWindow); - baseWin.visibility = false; - }); - - progress.total = windowPromises.size; - progress.current = 0; - - // We'll iterate through the Promise array, yielding each one, so as to - // provide useful progress information to AsyncShutdown. - for (let [win, promise] of windowPromises) { - yield promise; - this._collectWindowData(win); - progress.current++; - }; - - // We must cache this because _getMostRecentBrowserWindow will always - // return null by the time quit-application occurs. - var activeWindow = this._getMostRecentBrowserWindow(); - if (activeWindow) - this.activeWindowSSiCache = activeWindow.__SSi || ""; - DirtyWindows.clear(); - }), - - /** - * On last browser window close - */ - onLastWindowCloseGranted: function ssi_onLastWindowCloseGranted() { - // last browser window is quitting. - // remember to restore the last window when another browser window is opened - // do not account for pref(resume_session_once) at this point, as it might be - // set by another observer getting this notice after us - this._restoreLastWindow = true; - }, - - /** - * On quitting application - * @param aData - * String type of quitting - */ - onQuitApplication: function ssi_onQuitApplication(aData) { - if (aData == "restart") { - this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); - // The browser:purge-session-history notification fires after the - // quit-application notification so unregister the - // browser:purge-session-history notification to prevent clearing - // session data on disk on a restart. It is also unnecessary to - // perform any other sanitization processing on a restart as the - // browser is about to exit anyway. - Services.obs.removeObserver(this, "browser:purge-session-history"); - } - - if (aData != "restart") { - // Throw away the previous session on shutdown - LastSession.clear(); - } - - this._uninit(); - }, - - /** - * On purge of session history - */ - onPurgeSessionHistory: function ssi_onPurgeSessionHistory() { - SessionFile.wipe(); - // If the browser is shutting down, simply return after clearing the - // session data on disk as this notification fires after the - // quit-application notification so the browser is about to exit. - if (RunState.isQuitting) - return; - LastSession.clear(); - - let openWindows = {}; - // Collect open windows. - this._forEachBrowserWindow(({__SSi: id}) => openWindows[id] = true); - - // also clear all data about closed tabs and windows - for (let ix in this._windows) { - if (ix in openWindows) { - this._windows[ix]._closedTabs = []; - } else { - delete this._windows[ix]; - } - } - // also clear all data about closed windows - this._closedWindows = []; - // give the tabbrowsers a chance to clear their histories first - var win = this._getMostRecentBrowserWindow(); - if (win) { - win.setTimeout(() => SessionSaver.run(), 0); - } else if (RunState.isRunning) { - SessionSaver.run(); - } - - this._clearRestoringWindows(); - this._saveableClosedWindowData = new WeakSet(); - }, - - /** - * On purge of domain data - * @param aData - * String domain data - */ - onPurgeDomainData: function ssi_onPurgeDomainData(aData) { - // does a session history entry contain a url for the given domain? - function containsDomain(aEntry) { - if (Utils.hasRootDomain(aEntry.url, aData)) { - return true; - } - return aEntry.children && aEntry.children.some(containsDomain, this); - } - // remove all closed tabs containing a reference to the given domain - for (let ix in this._windows) { - let closedTabs = this._windows[ix]._closedTabs; - for (let i = closedTabs.length - 1; i >= 0; i--) { - if (closedTabs[i].state.entries.some(containsDomain, this)) - closedTabs.splice(i, 1); - } - } - // remove all open & closed tabs containing a reference to the given - // domain in closed windows - for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) { - let closedTabs = this._closedWindows[ix]._closedTabs; - let openTabs = this._closedWindows[ix].tabs; - let openTabCount = openTabs.length; - for (let i = closedTabs.length - 1; i >= 0; i--) - if (closedTabs[i].state.entries.some(containsDomain, this)) - closedTabs.splice(i, 1); - for (let j = openTabs.length - 1; j >= 0; j--) { - if (openTabs[j].entries.some(containsDomain, this)) { - openTabs.splice(j, 1); - if (this._closedWindows[ix].selected > j) - this._closedWindows[ix].selected--; - } - } - if (openTabs.length == 0) { - this._closedWindows.splice(ix, 1); - } - else if (openTabs.length != openTabCount) { - // Adjust the window's title if we removed an open tab - let selectedTab = openTabs[this._closedWindows[ix].selected - 1]; - // some duplication from restoreHistory - make sure we get the correct title - let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; - if (activeIndex >= selectedTab.entries.length) - activeIndex = selectedTab.entries.length - 1; - this._closedWindows[ix].title = selectedTab.entries[activeIndex].title; - } - } - - if (RunState.isRunning) { - SessionSaver.run(); - } - - this._clearRestoringWindows(); - }, - - /** - * On preference change - * @param aData - * String preference changed - */ - onPrefChange: function ssi_onPrefChange(aData) { - switch (aData) { - // if the user decreases the max number of closed tabs they want - // preserved update our internal states to match that max - case "sessionstore.max_tabs_undo": - this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); - for (let ix in this._windows) { - this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length); - } - break; - case "sessionstore.max_windows_undo": - this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); - this._capClosedWindows(); - break; - } - }, - - /** - * save state when new tab is added - * @param aWindow - * Window reference - */ - onTabAdd: function ssi_onTabAdd(aWindow) { - this.saveStateDelayed(aWindow); - }, - - /** - * set up listeners for a new tab - * @param aWindow - * Window reference - * @param aTab - * Tab reference - */ - onTabBrowserInserted: function ssi_onTabBrowserInserted(aWindow, aTab) { - let browser = aTab.linkedBrowser; - browser.addEventListener("SwapDocShells", this); - browser.addEventListener("oop-browser-crashed", this); - - if (browser.frameLoader) { - this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader); - } - }, - - /** - * remove listeners for a tab - * @param aWindow - * Window reference - * @param aTab - * Tab reference - * @param aNoNotification - * bool Do not save state if we're updating an existing tab - */ - onTabRemove: function ssi_onTabRemove(aWindow, aTab, aNoNotification) { - let browser = aTab.linkedBrowser; - browser.removeEventListener("SwapDocShells", this); - browser.removeEventListener("oop-browser-crashed", this); - - // If this tab was in the middle of restoring or still needs to be restored, - // we need to reset that state. If the tab was restoring, we will attempt to - // restore the next tab. - let previousState = browser.__SS_restoreState; - if (previousState) { - this._resetTabRestoringState(aTab); - if (previousState == TAB_STATE_RESTORING) - this.restoreNextTab(); - } - - if (!aNoNotification) { - this.saveStateDelayed(aWindow); - } - }, - - /** - * When a tab closes, collect its properties - * @param aWindow - * Window reference - * @param aTab - * Tab reference - */ - onTabClose: function ssi_onTabClose(aWindow, aTab) { - // notify the tabbrowser that the tab state will be retrieved for the last time - // (so that extension authors can easily set data on soon-to-be-closed tabs) - var event = aWindow.document.createEvent("Events"); - event.initEvent("SSTabClosing", true, false); - aTab.dispatchEvent(event); - - // don't update our internal state if we don't have to - if (this._max_tabs_undo == 0) { - return; - } - - // Get the latest data for this tab (generally, from the cache) - let tabState = TabState.collect(aTab); - - // Don't save private tabs - let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow); - if (!isPrivateWindow && tabState.isPrivate) { - return; - } - - // Store closed-tab data for undo. - let tabbrowser = aWindow.gBrowser; - let tabTitle = this._replaceLoadingTitle(aTab.label, tabbrowser, aTab); - let {permanentKey} = aTab.linkedBrowser; - - let tabData = { - permanentKey, - state: tabState, - title: tabTitle, - image: tabbrowser.getIcon(aTab), - iconLoadingPrincipal: Utils.serializePrincipal(aTab.linkedBrowser.contentPrincipal), - pos: aTab._tPos, - closedAt: Date.now() - }; - - let closedTabs = this._windows[aWindow.__SSi]._closedTabs; - - // Determine whether the tab contains any information worth saving. Note - // that there might be pending state changes queued in the child that - // didn't reach the parent yet. If a tab is emptied before closing then we - // might still remove it from the list of closed tabs later. - if (this._shouldSaveTabState(tabState)) { - // Save the tab state, for now. We might push a valid tab out - // of the list but those cases should be extremely rare and - // do probably never occur when using the browser normally. - // (Tests or add-ons might do weird things though.) - this.saveClosedTabData(closedTabs, tabData); - } - - // Remember the closed tab to properly handle any last updates included in - // the final "update" message sent by the frame script's unload handler. - this._closedTabs.set(permanentKey, {closedTabs, tabData}); - }, - - /** - * Insert a given |tabData| object into the list of |closedTabs|. We will - * determine the right insertion point based on the .closedAt properties of - * all tabs already in the list. The list will be truncated to contain a - * maximum of |this._max_tabs_undo| entries. - * - * @param closedTabs (array) - * The list of closed tabs for a window. - * @param tabData (object) - * The tabData to be inserted. - */ - saveClosedTabData(closedTabs, tabData) { - // Find the index of the first tab in the list - // of closed tabs that was closed before our tab. - let index = closedTabs.findIndex(tab => { - return tab.closedAt < tabData.closedAt; - }); - - // If we found no tab closed before our - // tab then just append it to the list. - if (index == -1) { - index = closedTabs.length; - } - - // About to save the closed tab, add a unique ID. - tabData.closedId = this._nextClosedId++; - - // Insert tabData at the right position. - closedTabs.splice(index, 0, tabData); - - // Truncate the list of closed tabs, if needed. - if (closedTabs.length > this._max_tabs_undo) { - closedTabs.splice(this._max_tabs_undo, closedTabs.length); - } - }, - - /** - * Remove the closed tab data at |index| from the list of |closedTabs|. If - * the tab's final message is still pending we will simply discard it when - * it arrives so that the tab doesn't reappear in the list. - * - * @param closedTabs (array) - * The list of closed tabs for a window. - * @param index (uint) - * The index of the tab to remove. - */ - removeClosedTabData(closedTabs, index) { - // Remove the given index from the list. - let [closedTab] = closedTabs.splice(index, 1); - - // If the closed tab's state still has a .permanentKey property then we - // haven't seen its final update message yet. Remove it from the map of - // closed tabs so that we will simply discard its last messages and will - // not add it back to the list of closed tabs again. - if (closedTab.permanentKey) { - this._closedTabs.delete(closedTab.permanentKey); - this._closedWindowTabs.delete(closedTab.permanentKey); - delete closedTab.permanentKey; - } - - return closedTab; - }, - - /** - * When a tab is selected, save session data - * @param aWindow - * Window reference - */ - onTabSelect: function ssi_onTabSelect(aWindow) { - if (RunState.isRunning) { - this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex; - - let tab = aWindow.gBrowser.selectedTab; - let browser = tab.linkedBrowser; - - if (browser.__SS_restoreState && - browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { - // If __SS_restoreState is still on the browser and it is - // TAB_STATE_NEEDS_RESTORE, then then we haven't restored - // this tab yet. - // - // It's possible that this tab was recently revived, and that - // we've deferred showing the tab crashed page for it (if the - // tab crashed in the background). If so, we need to re-enter - // the crashed state, since we'll be showing the tab crashed - // page. - if (TabCrashHandler.willShowCrashedTab(browser)) { - this.enterCrashedState(browser); - } else { - this.restoreTabContent(tab); - } - } - } - }, - - onTabShow: function ssi_onTabShow(aWindow, aTab) { - // If the tab hasn't been restored yet, move it into the right bucket - if (aTab.linkedBrowser.__SS_restoreState && - aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { - TabRestoreQueue.hiddenToVisible(aTab); - - // let's kick off tab restoration again to ensure this tab gets restored - // with "restore_hidden_tabs" == false (now that it has become visible) - this.restoreNextTab(); - } - - // Default delay of 2 seconds gives enough time to catch multiple TabShow - // events. This used to be due to changing groups in 'tab groups'. We - // might be able to get rid of this now? - this.saveStateDelayed(aWindow); - }, - - onTabHide: function ssi_onTabHide(aWindow, aTab) { - // If the tab hasn't been restored yet, move it into the right bucket - if (aTab.linkedBrowser.__SS_restoreState && - aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { - TabRestoreQueue.visibleToHidden(aTab); - } - - // Default delay of 2 seconds gives enough time to catch multiple TabHide - // events. This used to be due to changing groups in 'tab groups'. We - // might be able to get rid of this now? - this.saveStateDelayed(aWindow); - }, - - /** - * Handler for the event that is fired when a <xul:browser> crashes. - * - * @param aWindow - * The window that the crashed browser belongs to. - * @param aBrowser - * The <xul:browser> that is now in the crashed state. - */ - onBrowserCrashed: function(aBrowser) { - NS_ASSERT(aBrowser.isRemoteBrowser, - "Only remote browsers should be able to crash"); - - this.enterCrashedState(aBrowser); - // The browser crashed so we might never receive flush responses. - // Resolve all pending flush requests for the crashed browser. - TabStateFlusher.resolveAll(aBrowser); - }, - - /** - * Called when a browser is showing or is about to show the tab - * crashed page. This method causes SessionStore to ignore the - * tab until it's restored. - * - * @param browser - * The <xul:browser> that is about to show the crashed page. - */ - enterCrashedState(browser) { - this._crashedBrowsers.add(browser.permanentKey); - - let win = browser.ownerGlobal; - - // If we hadn't yet restored, or were still in the midst of - // restoring this browser at the time of the crash, we need - // to reset its state so that we can try to restore it again - // when the user revives the tab from the crash. - if (browser.__SS_restoreState) { - let tab = win.gBrowser.getTabForBrowser(browser); - this._resetLocalTabRestoringState(tab); - } - }, - - // Clean up data that has been closed a long time ago. - // Do not reschedule a save. This will wait for the next regular - // save. - onIdleDaily: function() { - // Remove old closed windows - this._cleanupOldData([this._closedWindows]); - - // Remove closed tabs of closed windows - this._cleanupOldData(this._closedWindows.map((winData) => winData._closedTabs)); - - // Remove closed tabs of open windows - this._cleanupOldData(Object.keys(this._windows).map((key) => this._windows[key]._closedTabs)); - }, - - // Remove "old" data from an array - _cleanupOldData: function(targets) { - const TIME_TO_LIVE = this._prefBranch.getIntPref("sessionstore.cleanup.forget_closed_after"); - const now = Date.now(); - - for (let array of targets) { - for (let i = array.length - 1; i >= 0; --i) { - let data = array[i]; - // Make sure that we have a timestamp to tell us when the target - // has been closed. If we don't have a timestamp, default to a - // safe timestamp: just now. - data.closedAt = data.closedAt || now; - if (now - data.closedAt > TIME_TO_LIVE) { - array.splice(i, 1); - } - } - } - }, - - /* ........ nsISessionStore API .............. */ - - getBrowserState: function ssi_getBrowserState() { - let state = this.getCurrentState(); - - // Don't include the last session state in getBrowserState(). - delete state.lastSessionState; - - // Don't include any deferred initial state. - delete state.deferredInitialState; - - return JSON.stringify(state); - }, - - setBrowserState: function ssi_setBrowserState(aState) { - this._handleClosedWindows(); - - try { - var state = JSON.parse(aState); - } - catch (ex) { /* invalid state object - don't restore anything */ } - if (!state) { - throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG); - } - if (!state.windows) { - throw Components.Exception("No windows", Cr.NS_ERROR_INVALID_ARG); - } - - this._browserSetState = true; - - // Make sure the priority queue is emptied out - this._resetRestoringState(); - - var window = this._getMostRecentBrowserWindow(); - if (!window) { - this._restoreCount = 1; - this._openWindowWithState(state); - return; - } - - // close all other browser windows - this._forEachBrowserWindow(function(aWindow) { - if (aWindow != window) { - aWindow.close(); - this.onClose(aWindow); - } - }); - - // make sure closed window data isn't kept - this._closedWindows = []; - - // determine how many windows are meant to be restored - this._restoreCount = state.windows ? state.windows.length : 0; - - // global data must be restored before restoreWindow is called so that - // it happens before observers are notified - this._globalState.setFromState(state); - - // restore to the given state - this.restoreWindows(window, state, {overwriteTabs: true}); - }, - - getWindowState: function ssi_getWindowState(aWindow) { - if ("__SSi" in aWindow) { - return JSON.stringify(this._getWindowState(aWindow)); - } - - if (DyingWindowCache.has(aWindow)) { - let data = DyingWindowCache.get(aWindow); - return JSON.stringify({ windows: [data] }); - } - - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - }, - - setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) { - if (!aWindow.__SSi) { - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - - this.restoreWindows(aWindow, aState, {overwriteTabs: aOverwrite}); - }, - - getTabState: function ssi_getTabState(aTab) { - if (!aTab.ownerGlobal.__SSi) { - throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - - let tabState = TabState.collect(aTab); - - return JSON.stringify(tabState); - }, - - setTabState(aTab, aState) { - // Remove the tab state from the cache. - // Note that we cannot simply replace the contents of the cache - // as |aState| can be an incomplete state that will be completed - // by |restoreTabs|. - let tabState = JSON.parse(aState); - if (!tabState) { - throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG); - } - if (typeof tabState != "object") { - throw Components.Exception("Not an object", Cr.NS_ERROR_INVALID_ARG); - } - if (!("entries" in tabState)) { - throw Components.Exception("Invalid state object: no entries", Cr.NS_ERROR_INVALID_ARG); - } - - let window = aTab.ownerGlobal; - if (!("__SSi" in window)) { - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - - if (aTab.linkedBrowser.__SS_restoreState) { - this._resetTabRestoringState(aTab); - } - - this.restoreTab(aTab, tabState); - }, - - duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0, aRestoreImmediately = true) { - if (!aTab.ownerGlobal.__SSi) { - throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - if (!aWindow.gBrowser) { - throw Components.Exception("Invalid window object: no gBrowser", Cr.NS_ERROR_INVALID_ARG); - } - - // Create a new tab. - let userContextId = aTab.getAttribute("usercontextid"); - let newTab = aTab == aWindow.gBrowser.selectedTab ? - aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab, userContextId}) : - aWindow.gBrowser.addTab(null, {userContextId}); - - // Set tab title to "Connecting..." and start the throbber to pretend we're - // doing something while actually waiting for data from the frame script. - aWindow.gBrowser.setTabTitleLoading(newTab); - newTab.setAttribute("busy", "true"); - - // Collect state before flushing. - let tabState = TabState.clone(aTab); - - // Flush to get the latest tab state to duplicate. - let browser = aTab.linkedBrowser; - TabStateFlusher.flush(browser).then(() => { - // The new tab might have been closed in the meantime. - if (newTab.closing || !newTab.linkedBrowser) { - return; - } - - let window = newTab.ownerGlobal; - - // The tab or its window might be gone. - if (!window || !window.__SSi) { - return; - } - - // Update state with flushed data. We can't use TabState.clone() here as - // the tab to duplicate may have already been closed. In that case we - // only have access to the <xul:browser>. - let options = {includePrivateData: true}; - TabState.copyFromCache(browser, tabState, options); - - tabState.index += aDelta; - tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); - tabState.pinned = false; - - // Restore the state into the new tab. - this.restoreTab(newTab, tabState, { - restoreImmediately: aRestoreImmediately - }); - }); - - return newTab; - }, - - getClosedTabCount: function ssi_getClosedTabCount(aWindow) { - if ("__SSi" in aWindow) { - return this._windows[aWindow.__SSi]._closedTabs.length; - } - - if (!DyingWindowCache.has(aWindow)) { - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - - return DyingWindowCache.get(aWindow)._closedTabs.length; - }, - - getClosedTabData: function ssi_getClosedTabData(aWindow, aAsString = true) { - if ("__SSi" in aWindow) { - return aAsString ? - JSON.stringify(this._windows[aWindow.__SSi]._closedTabs) : - Cu.cloneInto(this._windows[aWindow.__SSi]._closedTabs, {}); - } - - if (!DyingWindowCache.has(aWindow)) { - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - - let data = DyingWindowCache.get(aWindow); - return aAsString ? JSON.stringify(data._closedTabs) : Cu.cloneInto(data._closedTabs, {}); - }, - - undoCloseTab: function ssi_undoCloseTab(aWindow, aIndex) { - if (!aWindow.__SSi) { - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - - var closedTabs = this._windows[aWindow.__SSi]._closedTabs; - - // default to the most-recently closed tab - aIndex = aIndex || 0; - if (!(aIndex in closedTabs)) { - throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG); - } - - // fetch the data of closed tab, while removing it from the array - let {state, pos} = this.removeClosedTabData(closedTabs, aIndex); - - // create a new tab - let tabbrowser = aWindow.gBrowser; - let tab = tabbrowser.selectedTab = tabbrowser.addTab(null, state); - - // restore tab content - this.restoreTab(tab, state); - - // restore the tab's position - tabbrowser.moveTabTo(tab, pos); - - // focus the tab's content area (bug 342432) - tab.linkedBrowser.focus(); - - return tab; - }, - - forgetClosedTab: function ssi_forgetClosedTab(aWindow, aIndex) { - if (!aWindow.__SSi) { - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - - var closedTabs = this._windows[aWindow.__SSi]._closedTabs; - - // default to the most-recently closed tab - aIndex = aIndex || 0; - if (!(aIndex in closedTabs)) { - throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG); - } - - // remove closed tab from the array - this.removeClosedTabData(closedTabs, aIndex); - }, - - getClosedWindowCount: function ssi_getClosedWindowCount() { - return this._closedWindows.length; - }, - - getClosedWindowData: function ssi_getClosedWindowData(aAsString = true) { - return aAsString ? JSON.stringify(this._closedWindows) : Cu.cloneInto(this._closedWindows, {}); - }, - - undoCloseWindow: function ssi_undoCloseWindow(aIndex) { - if (!(aIndex in this._closedWindows)) { - throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG); - } - - // reopen the window - let state = { windows: this._closedWindows.splice(aIndex, 1) }; - delete state.windows[0].closedAt; // Window is now open. - - let window = this._openWindowWithState(state); - this.windowToFocus = window; - return window; - }, - - forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) { - // default to the most-recently closed window - aIndex = aIndex || 0; - if (!(aIndex in this._closedWindows)) { - throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG); - } - - // remove closed window from the array - let winData = this._closedWindows[aIndex]; - this._closedWindows.splice(aIndex, 1); - this._saveableClosedWindowData.delete(winData); - }, - - getWindowValue: function ssi_getWindowValue(aWindow, aKey) { - if ("__SSi" in aWindow) { - var data = this._windows[aWindow.__SSi].extData || {}; - return data[aKey] || ""; - } - - if (DyingWindowCache.has(aWindow)) { - let data = DyingWindowCache.get(aWindow).extData || {}; - return data[aKey] || ""; - } - - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - }, - - setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) { - if (typeof aStringValue != "string") { - throw new TypeError("setWindowValue only accepts string values"); - } - - if (!("__SSi" in aWindow)) { - throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG); - } - if (!this._windows[aWindow.__SSi].extData) { - this._windows[aWindow.__SSi].extData = {}; - } - this._windows[aWindow.__SSi].extData[aKey] = aStringValue; - this.saveStateDelayed(aWindow); - }, - - deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) { - if (aWindow.__SSi && this._windows[aWindow.__SSi].extData && - this._windows[aWindow.__SSi].extData[aKey]) - delete this._windows[aWindow.__SSi].extData[aKey]; - this.saveStateDelayed(aWindow); - }, - - getTabValue: function ssi_getTabValue(aTab, aKey) { - return (aTab.__SS_extdata || {})[aKey] || ""; - }, - - setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) { - if (typeof aStringValue != "string") { - throw new TypeError("setTabValue only accepts string values"); - } - - // If the tab hasn't been restored, then set the data there, otherwise we - // could lose newly added data. - if (!aTab.__SS_extdata) { - aTab.__SS_extdata = {}; - } - - aTab.__SS_extdata[aKey] = aStringValue; - this.saveStateDelayed(aTab.ownerGlobal); - }, - - deleteTabValue: function ssi_deleteTabValue(aTab, aKey) { - if (aTab.__SS_extdata && aKey in aTab.__SS_extdata) { - delete aTab.__SS_extdata[aKey]; - this.saveStateDelayed(aTab.ownerGlobal); - } - }, - - getGlobalValue: function ssi_getGlobalValue(aKey) { - return this._globalState.get(aKey); - }, - - setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) { - if (typeof aStringValue != "string") { - throw new TypeError("setGlobalValue only accepts string values"); - } - - this._globalState.set(aKey, aStringValue); - this.saveStateDelayed(); - }, - - deleteGlobalValue: function ssi_deleteGlobalValue(aKey) { - this._globalState.delete(aKey); - this.saveStateDelayed(); - }, - - persistTabAttribute: function ssi_persistTabAttribute(aName) { - if (TabAttributes.persist(aName)) { - this.saveStateDelayed(); - } - }, - - - /** - * Undoes the closing of a tab or window which corresponds - * to the closedId passed in. - * - * @param aClosedId - * The closedId of the tab or window - * - * @returns a tab or window object - */ - undoCloseById(aClosedId) { - // Check for a window first. - for (let i = 0, l = this._closedWindows.length; i < l; i++) { - if (this._closedWindows[i].closedId == aClosedId) { - return this.undoCloseWindow(i); - } - } - - // Check for a tab. - let windowsEnum = Services.wm.getEnumerator("navigator:browser"); - while (windowsEnum.hasMoreElements()) { - let window = windowsEnum.getNext(); - let windowState = this._windows[window.__SSi]; - if (windowState) { - for (let j = 0, l = windowState._closedTabs.length; j < l; j++) { - if (windowState._closedTabs[j].closedId == aClosedId) { - return this.undoCloseTab(window, j); - } - } - } - } - - // Neither a tab nor a window was found, return undefined and let the caller decide what to do about it. - return undefined; - }, - - /** - * Restores the session state stored in LastSession. This will attempt - * to merge data into the current session. If a window was opened at startup - * with pinned tab(s), then the remaining data from the previous session for - * that window will be opened into that window. Otherwise new windows will - * be opened. - */ - restoreLastSession: function ssi_restoreLastSession() { - // Use the public getter since it also checks PB mode - if (!this.canRestoreLastSession) { - throw Components.Exception("Last session can not be restored"); - } - - Services.obs.notifyObservers(null, NOTIFY_INITIATING_MANUAL_RESTORE, ""); - - // First collect each window with its id... - let windows = {}; - this._forEachBrowserWindow(function(aWindow) { - if (aWindow.__SS_lastSessionWindowID) - windows[aWindow.__SS_lastSessionWindowID] = aWindow; - }); - - let lastSessionState = LastSession.getState(); - - // This shouldn't ever be the case... - if (!lastSessionState.windows.length) { - throw Components.Exception("lastSessionState has no windows", Cr.NS_ERROR_UNEXPECTED); - } - - // We're technically doing a restore, so set things up so we send the - // notification when we're done. We want to send "sessionstore-browser-state-restored". - this._restoreCount = lastSessionState.windows.length; - this._browserSetState = true; - - // We want to re-use the last opened window instead of opening a new one in - // the case where it's "empty" and not associated with a window in the session. - // We will do more processing via _prepWindowToRestoreInto if we need to use - // the lastWindow. - let lastWindow = this._getMostRecentBrowserWindow(); - let canUseLastWindow = lastWindow && - !lastWindow.__SS_lastSessionWindowID; - - // global data must be restored before restoreWindow is called so that - // it happens before observers are notified - this._globalState.setFromState(lastSessionState); - - // Restore into windows or open new ones as needed. - for (let i = 0; i < lastSessionState.windows.length; i++) { - let winState = lastSessionState.windows[i]; - let lastSessionWindowID = winState.__lastSessionWindowID; - // delete lastSessionWindowID so we don't add that to the window again - delete winState.__lastSessionWindowID; - - // See if we can use an open window. First try one that is associated with - // the state we're trying to restore and then fallback to the last selected - // window. - let windowToUse = windows[lastSessionWindowID]; - if (!windowToUse && canUseLastWindow) { - windowToUse = lastWindow; - canUseLastWindow = false; - } - - let [canUseWindow, canOverwriteTabs] = this._prepWindowToRestoreInto(windowToUse); - - // If there's a window already open that we can restore into, use that - if (canUseWindow) { - // Since we're not overwriting existing tabs, we want to merge _closedTabs, - // putting existing ones first. Then make sure we're respecting the max pref. - if (winState._closedTabs && winState._closedTabs.length) { - let curWinState = this._windows[windowToUse.__SSi]; - curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs); - curWinState._closedTabs.splice(this._max_tabs_undo, curWinState._closedTabs.length); - } - - // Restore into that window - pretend it's a followup since we'll already - // have a focused window. - //XXXzpao This is going to merge extData together (taking what was in - // winState over what is in the window already. - let options = {overwriteTabs: canOverwriteTabs, isFollowUp: true}; - this.restoreWindow(windowToUse, winState, options); - } - else { - this._openWindowWithState({ windows: [winState] }); - } - } - - // Merge closed windows from this session with ones from last session - if (lastSessionState._closedWindows) { - this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows); - this._capClosedWindows(); - } - - // Scratchpad - if (lastSessionState.scratchpads) { - ScratchpadManager.restoreSession(lastSessionState.scratchpads); - } - - // The Browser Console - if (lastSessionState.browserConsole) { - HUDService.restoreBrowserConsoleSession(); - } - - // Set data that persists between sessions - this._recentCrashes = lastSessionState.session && - lastSessionState.session.recentCrashes || 0; - - // Update the session start time using the restored session state. - this._updateSessionStartTime(lastSessionState); - - LastSession.clear(); - }, - - /** - * Revive a crashed tab and restore its state from before it crashed. - * - * @param aTab - * A <xul:tab> linked to a crashed browser. This is a no-op if the - * browser hasn't actually crashed, or is not associated with a tab. - * This function will also throw if the browser happens to be remote. - */ - reviveCrashedTab(aTab) { - if (!aTab) { - throw new Error("SessionStore.reviveCrashedTab expected a tab, but got null."); - } - - let browser = aTab.linkedBrowser; - if (!this._crashedBrowsers.has(browser.permanentKey)) { - return; - } - - // Sanity check - the browser to be revived should not be remote - // at this point. - if (browser.isRemoteBrowser) { - throw new Error("SessionStore.reviveCrashedTab: " + - "Somehow a crashed browser is still remote.") - } - - // We put the browser at about:blank in case the user is - // restoring tabs on demand. This way, the user won't see - // a flash of the about:tabcrashed page after selecting - // the revived tab. - aTab.removeAttribute("crashed"); - browser.loadURI("about:blank", null, null); - - let data = TabState.collect(aTab); - this.restoreTab(aTab, data, { - forceOnDemand: true, - }); - }, - - /** - * Revive all crashed tabs and reset the crashed tabs count to 0. - */ - reviveAllCrashedTabs() { - let windowsEnum = Services.wm.getEnumerator("navigator:browser"); - while (windowsEnum.hasMoreElements()) { - let window = windowsEnum.getNext(); - for (let tab of window.gBrowser.tabs) { - this.reviveCrashedTab(tab); - } - } - }, - - /** - * Navigate the given |tab| by first collecting its current state and then - * either changing only the index of the currently shown history entry, - * or restoring the exact same state again and passing the new URL to load - * in |loadArguments|. Use this method to seamlessly switch between pages - * loaded in the parent and pages loaded in the child process. - * - * This method might be called multiple times before it has finished - * flushing the browser tab. If that occurs, the loadArguments from - * the most recent call to navigateAndRestore will be used once the - * flush has finished. - */ - navigateAndRestore(tab, loadArguments, historyIndex) { - let window = tab.ownerGlobal; - NS_ASSERT(window.__SSi, "tab's window must be tracked"); - let browser = tab.linkedBrowser; - - // Were we already waiting for a flush from a previous call to - // navigateAndRestore on this tab? - let alreadyRestoring = - this._remotenessChangingBrowsers.has(browser.permanentKey); - - // Stash the most recent loadArguments in this WeakMap so that - // we know to use it when the TabStateFlusher.flush resolves. - this._remotenessChangingBrowsers.set(browser.permanentKey, loadArguments); - - if (alreadyRestoring) { - // This tab was already being restored to run in the - // correct process. We're done here. - return; - } - - // Set tab title to "Connecting..." and start the throbber to pretend we're - // doing something while actually waiting for data from the frame script. - window.gBrowser.setTabTitleLoading(tab); - tab.setAttribute("busy", "true"); - - // Flush to get the latest tab state. - TabStateFlusher.flush(browser).then(() => { - // loadArguments might have been overwritten by multiple calls - // to navigateAndRestore while we waited for the tab to flush, - // so we use the most recently stored one. - let recentLoadArguments = - this._remotenessChangingBrowsers.get(browser.permanentKey); - this._remotenessChangingBrowsers.delete(browser.permanentKey); - - // The tab might have been closed/gone in the meantime. - if (tab.closing || !tab.linkedBrowser) { - return; - } - - let window = tab.ownerGlobal; - - // The tab or its window might be gone. - if (!window || !window.__SSi || window.closed) { - return; - } - - let tabState = TabState.clone(tab); - let options = { - restoreImmediately: true, - // We want to make sure that this information is passed to restoreTab - // whether or not a historyIndex is passed in. Thus, we extract it from - // the loadArguments. - reloadInFreshProcess: !!recentLoadArguments.reloadInFreshProcess, - }; - - if (historyIndex >= 0) { - tabState.index = historyIndex + 1; - tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); - } else { - options.loadArguments = recentLoadArguments; - } - - // Need to reset restoring tabs. - if (tab.linkedBrowser.__SS_restoreState) { - this._resetLocalTabRestoringState(tab); - } - - // Restore the state into the tab. - this.restoreTab(tab, tabState, options); - }); - - tab.linkedBrowser.__SS_restoreState = TAB_STATE_WILL_RESTORE; - }, - - /** - * Retrieves the latest session history information for a tab. The cached data - * is returned immediately, but a callback may be provided that supplies - * up-to-date data when or if it is available. The callback is passed a single - * argument with data in the same format as the return value. - * - * @param tab tab to retrieve the session history for - * @param updatedCallback function to call with updated data as the single argument - * @returns a object containing 'index' specifying the current index, and an - * array 'entries' containing an object for each history item. - */ - getSessionHistory(tab, updatedCallback) { - if (updatedCallback) { - TabStateFlusher.flush(tab.linkedBrowser).then(() => { - let sessionHistory = this.getSessionHistory(tab); - if (sessionHistory) { - updatedCallback(sessionHistory); - } - }); - } - - // Don't continue if the tab was closed before TabStateFlusher.flush resolves. - if (tab.linkedBrowser) { - let tabState = TabState.collect(tab); - return { index: tabState.index - 1, entries: tabState.entries } - } - }, - - /** - * See if aWindow is usable for use when restoring a previous session via - * restoreLastSession. If usable, prepare it for use. - * - * @param aWindow - * the window to inspect & prepare - * @returns [canUseWindow, canOverwriteTabs] - * canUseWindow: can the window be used to restore into - * canOverwriteTabs: all of the current tabs are home pages and we - * can overwrite them - */ - _prepWindowToRestoreInto: function ssi_prepWindowToRestoreInto(aWindow) { - if (!aWindow) - return [false, false]; - - // We might be able to overwrite the existing tabs instead of just adding - // the previous session's tabs to the end. This will be set if possible. - let canOverwriteTabs = false; - - // Look at the open tabs in comparison to home pages. If all the tabs are - // home pages then we'll end up overwriting all of them. Otherwise we'll - // just close the tabs that match home pages. Tabs with the about:blank - // URI will always be overwritten. - let homePages = ["about:blank"]; - let removableTabs = []; - let tabbrowser = aWindow.gBrowser; - let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs; - let startupPref = this._prefBranch.getIntPref("startup.page"); - if (startupPref == 1) - homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|")); - - for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) { - let tab = tabbrowser.tabs[i]; - if (homePages.indexOf(tab.linkedBrowser.currentURI.spec) != -1) { - removableTabs.push(tab); - } - } - - if (tabbrowser.tabs.length == removableTabs.length) { - canOverwriteTabs = true; - } - else { - // If we're not overwriting all of the tabs, then close the home tabs. - for (let i = removableTabs.length - 1; i >= 0; i--) { - tabbrowser.removeTab(removableTabs.pop(), { animate: false }); - } - } - - return [true, canOverwriteTabs]; - }, - - /* ........ Saving Functionality .............. */ - - /** - * Store window dimensions, visibility, sidebar - * @param aWindow - * Window reference - */ - _updateWindowFeatures: function ssi_updateWindowFeatures(aWindow) { - var winData = this._windows[aWindow.__SSi]; - - WINDOW_ATTRIBUTES.forEach(function(aAttr) { - winData[aAttr] = this._getWindowDimension(aWindow, aAttr); - }, this); - - var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) { - return aWindow[aItem] && !aWindow[aItem].visible; - }); - if (hidden.length != 0) - winData.hidden = hidden.join(","); - else if (winData.hidden) - delete winData.hidden; - - var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand"); - if (sidebar) - winData.sidebar = sidebar; - else if (winData.sidebar) - delete winData.sidebar; - }, - - /** - * gather session data as object - * @param aUpdateAll - * Bool update all windows - * @returns object - */ - getCurrentState: function (aUpdateAll) { - this._handleClosedWindows(); - - var activeWindow = this._getMostRecentBrowserWindow(); - - TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS"); - if (RunState.isRunning) { - // update the data for all windows with activities since the last save operation - this._forEachBrowserWindow(function(aWindow) { - if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore - return; - if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) { - this._collectWindowData(aWindow); - } - else { // always update the window features (whose change alone never triggers a save operation) - this._updateWindowFeatures(aWindow); - } - }); - DirtyWindows.clear(); - } - TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS"); - - // An array that at the end will hold all current window data. - var total = []; - // The ids of all windows contained in 'total' in the same order. - var ids = []; - // The number of window that are _not_ popups. - var nonPopupCount = 0; - var ix; - - // collect the data for all windows - for (ix in this._windows) { - if (this._windows[ix]._restoring) // window data is still in _statesToRestore - continue; - total.push(this._windows[ix]); - ids.push(ix); - if (!this._windows[ix].isPopup) - nonPopupCount++; - } - - TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS"); - SessionCookies.update(total); - TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS"); - - // collect the data for all windows yet to be restored - for (ix in this._statesToRestore) { - for (let winData of this._statesToRestore[ix].windows) { - total.push(winData); - if (!winData.isPopup) - nonPopupCount++; - } - } - - // shallow copy this._closedWindows to preserve current state - let lastClosedWindowsCopy = this._closedWindows.slice(); - - if (AppConstants.platform != "macosx") { - // If no non-popup browser window remains open, return the state of the last - // closed window(s). We only want to do this when we're actually "ending" - // the session. - //XXXzpao We should do this for _restoreLastWindow == true, but that has - // its own check for popups. c.f. bug 597619 - if (nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 && - RunState.isQuitting) { - // prepend the last non-popup browser window, so that if the user loads more tabs - // at startup we don't accidentally add them to a popup window - do { - total.unshift(lastClosedWindowsCopy.shift()) - } while (total[0].isPopup && lastClosedWindowsCopy.length > 0) - } - } - - if (activeWindow) { - this.activeWindowSSiCache = activeWindow.__SSi || ""; - } - ix = ids.indexOf(this.activeWindowSSiCache); - // We don't want to restore focus to a minimized window or a window which had all its - // tabs stripped out (doesn't exist). - if (ix != -1 && total[ix] && total[ix].sizemode == "minimized") - ix = -1; - - let session = { - lastUpdate: Date.now(), - startTime: this._sessionStartTime, - recentCrashes: this._recentCrashes - }; - - let state = { - version: ["sessionrestore", FORMAT_VERSION], - windows: total, - selectedWindow: ix + 1, - _closedWindows: lastClosedWindowsCopy, - session: session, - global: this._globalState.getState() - }; - - // Scratchpad - if (Cu.isModuleLoaded("resource://devtools/client/scratchpad/scratchpad-manager.jsm")) { - // get open Scratchpad window states too - let scratchpads = ScratchpadManager.getSessionState(); - if (scratchpads && scratchpads.length) { - state.scratchpads = scratchpads; - } - } - - // The Browser Console - state.browserConsole = HUDService.getBrowserConsoleSessionState(); - - // Persist the last session if we deferred restoring it - if (LastSession.canRestore) { - state.lastSessionState = LastSession.getState(); - } - - // If we were called by the SessionSaver and started with only a private - // window we want to pass the deferred initial state to not lose the - // previous session. - if (this._deferredInitialState) { - state.deferredInitialState = this._deferredInitialState; - } - - return state; - }, - - /** - * serialize session data for a window - * @param aWindow - * Window reference - * @returns string - */ - _getWindowState: function ssi_getWindowState(aWindow) { - if (!this._isWindowLoaded(aWindow)) - return this._statesToRestore[aWindow.__SS_restoreID]; - - if (RunState.isRunning) { - this._collectWindowData(aWindow); - } - - let windows = [this._windows[aWindow.__SSi]]; - SessionCookies.update(windows); - - return { windows: windows }; - }, - - /** - * Gathers data about a window and its tabs, and updates its - * entry in this._windows. - * - * @param aWindow - * Window references. - * @returns a Map mapping the browser tabs from aWindow to the tab - * entry that was put into the window data in this._windows. - */ - _collectWindowData: function ssi_collectWindowData(aWindow) { - let tabMap = new Map(); - - if (!this._isWindowLoaded(aWindow)) - return tabMap; - - let tabbrowser = aWindow.gBrowser; - let tabs = tabbrowser.tabs; - let winData = this._windows[aWindow.__SSi]; - let tabsData = winData.tabs = []; - - // update the internal state data for this window - for (let tab of tabs) { - let tabData = TabState.collect(tab); - tabMap.set(tab, tabData); - tabsData.push(tabData); - } - winData.selected = tabbrowser.mTabBox.selectedIndex + 1; - - this._updateWindowFeatures(aWindow); - - // Make sure we keep __SS_lastSessionWindowID around for cases like entering - // or leaving PB mode. - if (aWindow.__SS_lastSessionWindowID) - this._windows[aWindow.__SSi].__lastSessionWindowID = - aWindow.__SS_lastSessionWindowID; - - DirtyWindows.remove(aWindow); - return tabMap; - }, - - /* ........ Restoring Functionality .............. */ - - /** - * restore features to a single window - * @param aWindow - * Window reference to the window to use for restoration - * @param winData - * JS object - * @param aOptions - * {overwriteTabs: true} to overwrite existing tabs w/ new ones - * {isFollowUp: true} if this is not the restoration of the 1st window - * {firstWindow: true} if this is the first non-private window we're - * restoring in this session, that might open an - * external link as well - */ - restoreWindow: function ssi_restoreWindow(aWindow, winData, aOptions = {}) { - let overwriteTabs = aOptions && aOptions.overwriteTabs; - let isFollowUp = aOptions && aOptions.isFollowUp; - let firstWindow = aOptions && aOptions.firstWindow; - - if (isFollowUp) { - this.windowToFocus = aWindow; - } - - // initialize window if necessary - if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) - this.onLoad(aWindow); - - TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS"); - - // We're not returning from this before we end up calling restoreTabs - // for this window, so make sure we send the SSWindowStateBusy event. - this._setWindowStateBusy(aWindow); - - if (!winData.tabs) { - winData.tabs = []; - } - - // don't restore a single blank tab when we've had an external - // URL passed in for loading at startup (cf. bug 357419) - else if (firstWindow && !overwriteTabs && winData.tabs.length == 1 && - (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) { - winData.tabs = []; - } - - var tabbrowser = aWindow.gBrowser; - var openTabCount = overwriteTabs ? tabbrowser.browsers.length : -1; - var newTabCount = winData.tabs.length; - var tabs = []; - - // disable smooth scrolling while adding, moving, removing and selecting tabs - var tabstrip = tabbrowser.tabContainer.mTabstrip; - var smoothScroll = tabstrip.smoothScroll; - tabstrip.smoothScroll = false; - - // unpin all tabs to ensure they are not reordered in the next loop - if (overwriteTabs) { - for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--) - tabbrowser.unpinTab(tabbrowser.tabs[t]); - } - - // We need to keep track of the initially open tabs so that they - // can be moved to the end of the restored tabs. - let initialTabs = []; - if (!overwriteTabs && firstWindow) { - initialTabs = Array.slice(tabbrowser.tabs); - } - - // make sure that the selected tab won't be closed in order to - // prevent unnecessary flickering - if (overwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount) - tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1); - - let numVisibleTabs = 0; - - for (var t = 0; t < newTabCount; t++) { - // When trying to restore into existing tab, we also take the userContextId - // into account if present. - let userContextId = winData.tabs[t].userContextId; - let reuseExisting = t < openTabCount && - (tabbrowser.tabs[t].getAttribute("usercontextid") == (userContextId || "")); - // If the tab is pinned, then we'll be loading it right away, and - // there's no need to cause a remoteness flip by loading it initially - // non-remote. - let forceNotRemote = !winData.tabs[t].pinned; - let tab = reuseExisting ? tabbrowser.tabs[t] : - tabbrowser.addTab("about:blank", - {skipAnimation: true, - forceNotRemote, - userContextId}); - - // If we inserted a new tab because the userContextId didn't match with the - // open tab, even though `t < openTabCount`, we need to remove that open tab - // and put the newly added tab in its place. - if (!reuseExisting && t < openTabCount) { - tabbrowser.removeTab(tabbrowser.tabs[t]); - tabbrowser.moveTabTo(tab, t); - } - - tabs.push(tab); - - if (winData.tabs[t].pinned) - tabbrowser.pinTab(tabs[t]); - - if (winData.tabs[t].hidden) { - tabbrowser.hideTab(tabs[t]); - } - else { - tabbrowser.showTab(tabs[t]); - numVisibleTabs++; - } - - if (!!winData.tabs[t].muted != tabs[t].linkedBrowser.audioMuted) { - tabs[t].toggleMuteAudio(winData.tabs[t].muteReason); - } - } - - if (!overwriteTabs && firstWindow) { - // Move the originally open tabs to the end - let endPosition = tabbrowser.tabs.length - 1; - for (let i = 0; i < initialTabs.length; i++) { - tabbrowser.moveTabTo(initialTabs[i], endPosition); - } - } - - // if all tabs to be restored are hidden, make the first one visible - if (!numVisibleTabs && winData.tabs.length) { - winData.tabs[0].hidden = false; - tabbrowser.showTab(tabs[0]); - } - - // If overwriting tabs, we want to reset each tab's "restoring" state. Since - // we're overwriting those tabs, they should no longer be restoring. The - // tabs will be rebuilt and marked if they need to be restored after loading - // state (in restoreTabs). - if (overwriteTabs) { - for (let i = 0; i < tabbrowser.tabs.length; i++) { - let tab = tabbrowser.tabs[i]; - if (tabbrowser.browsers[i].__SS_restoreState) - this._resetTabRestoringState(tab); - } - } - - // We want to correlate the window with data from the last session, so - // assign another id if we have one. Otherwise clear so we don't do - // anything with it. - delete aWindow.__SS_lastSessionWindowID; - if (winData.__lastSessionWindowID) - aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID; - - // when overwriting tabs, remove all superflous ones - if (overwriteTabs && newTabCount < openTabCount) { - Array.slice(tabbrowser.tabs, newTabCount, openTabCount) - .forEach(tabbrowser.removeTab, tabbrowser); - } - - if (overwriteTabs) { - this.restoreWindowFeatures(aWindow, winData); - delete this._windows[aWindow.__SSi].extData; - } - if (winData.cookies) { - SessionCookies.restore(winData.cookies); - } - if (winData.extData) { - if (!this._windows[aWindow.__SSi].extData) { - this._windows[aWindow.__SSi].extData = {}; - } - for (var key in winData.extData) { - this._windows[aWindow.__SSi].extData[key] = winData.extData[key]; - } - } - - let newClosedTabsData = winData._closedTabs || []; - - if (overwriteTabs || firstWindow) { - // Overwrite existing closed tabs data when overwriteTabs=true - // or we're the first window to be restored. - this._windows[aWindow.__SSi]._closedTabs = newClosedTabsData; - } else if (this._max_tabs_undo > 0) { - // If we merge tabs, we also want to merge closed tabs data. We'll assume - // the restored tabs were closed more recently and append the current list - // of closed tabs to the new one... - newClosedTabsData = - newClosedTabsData.concat(this._windows[aWindow.__SSi]._closedTabs); - - // ... and make sure that we don't exceed the max number of closed tabs - // we can restore. - this._windows[aWindow.__SSi]._closedTabs = - newClosedTabsData.slice(0, this._max_tabs_undo); - } - - // Restore tabs, if any. - if (winData.tabs.length) { - this.restoreTabs(aWindow, tabs, winData.tabs, - (overwriteTabs ? (parseInt(winData.selected || "1")) : 0)); - } - - // set smoothScroll back to the original value - tabstrip.smoothScroll = smoothScroll; - - TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS"); - - this._setWindowStateReady(aWindow); - - this._sendWindowRestoredNotification(aWindow); - - Services.obs.notifyObservers(aWindow, NOTIFY_SINGLE_WINDOW_RESTORED, ""); - - this._sendRestoreCompletedNotifications(); - }, - - /** - * Restore multiple windows using the provided state. - * @param aWindow - * Window reference to the first window to use for restoration. - * Additionally required windows will be opened. - * @param aState - * JS object or JSON string - * @param aOptions - * {overwriteTabs: true} to overwrite existing tabs w/ new ones - * {isFollowUp: true} if this is not the restoration of the 1st window - * {firstWindow: true} if this is the first non-private window we're - * restoring in this session, that might open an - * external link as well - */ - restoreWindows: function ssi_restoreWindows(aWindow, aState, aOptions = {}) { - let isFollowUp = aOptions && aOptions.isFollowUp; - - if (isFollowUp) { - this.windowToFocus = aWindow; - } - - // initialize window if necessary - if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) - this.onLoad(aWindow); - - let root; - try { - root = (typeof aState == "string") ? JSON.parse(aState) : aState; - } - catch (ex) { // invalid state object - don't restore anything - debug(ex); - this._sendRestoreCompletedNotifications(); - return; - } - - // Restore closed windows if any. - if (root._closedWindows) { - this._closedWindows = root._closedWindows; - } - - // We're done here if there are no windows. - if (!root.windows || !root.windows.length) { - this._sendRestoreCompletedNotifications(); - return; - } - - if (!root.selectedWindow || root.selectedWindow > root.windows.length) { - root.selectedWindow = 0; - } - - // open new windows for all further window entries of a multi-window session - // (unless they don't contain any tab data) - let winData; - for (var w = 1; w < root.windows.length; w++) { - winData = root.windows[w]; - if (winData && winData.tabs && winData.tabs[0]) { - var window = this._openWindowWithState({ windows: [winData] }); - if (w == root.selectedWindow - 1) { - this.windowToFocus = window; - } - } - } - - this.restoreWindow(aWindow, root.windows[0], aOptions); - - // Scratchpad - if (aState.scratchpads) { - ScratchpadManager.restoreSession(aState.scratchpads); - } - - // The Browser Console - if (aState.browserConsole) { - HUDService.restoreBrowserConsoleSession(); - } - }, - - /** - * Manage history restoration for a window - * @param aWindow - * Window to restore the tabs into - * @param aTabs - * Array of tab references - * @param aTabData - * Array of tab data - * @param aSelectTab - * Index of the tab to select. This is a 1-based index where "1" - * indicates the first tab should be selected, and "0" indicates that - * the currently selected tab will not be changed. - */ - restoreTabs(aWindow, aTabs, aTabData, aSelectTab) { - var tabbrowser = aWindow.gBrowser; - - if (!this._isWindowLoaded(aWindow)) { - // from now on, the data will come from the actual window - delete this._statesToRestore[aWindow.__SS_restoreID]; - delete aWindow.__SS_restoreID; - delete this._windows[aWindow.__SSi]._restoring; - } - - let numTabsToRestore = aTabs.length; - let numTabsInWindow = tabbrowser.tabs.length; - let tabsDataArray = this._windows[aWindow.__SSi].tabs; - - // Update the window state in case we shut down without being notified. - // Individual tab states will be taken care of by restoreTab() below. - if (numTabsInWindow == numTabsToRestore) { - // Remove all previous tab data. - tabsDataArray.length = 0; - } else { - // Remove all previous tab data except tabs that should not be overriden. - tabsDataArray.splice(numTabsInWindow - numTabsToRestore); - } - - // Let the tab data array have the right number of slots. - tabsDataArray.length = numTabsInWindow; - - // If provided, set the selected tab. - if (aSelectTab > 0 && aSelectTab <= aTabs.length) { - tabbrowser.selectedTab = aTabs[aSelectTab - 1]; - - // Update the window state in case we shut down without being notified. - this._windows[aWindow.__SSi].selected = aSelectTab; - } - - // Restore all tabs. - for (let t = 0; t < aTabs.length; t++) { - this.restoreTab(aTabs[t], aTabData[t]); - } - }, - - // Restores the given tab state for a given tab. - restoreTab(tab, tabData, options = {}) { - NS_ASSERT(!tab.linkedBrowser.__SS_restoreState, - "must reset tab before calling restoreTab()"); - - let restoreImmediately = options.restoreImmediately; - let loadArguments = options.loadArguments; - let browser = tab.linkedBrowser; - let window = tab.ownerGlobal; - let tabbrowser = window.gBrowser; - let forceOnDemand = options.forceOnDemand; - let reloadInFreshProcess = options.reloadInFreshProcess; - - let willRestoreImmediately = restoreImmediately || - tabbrowser.selectedBrowser == browser || - loadArguments; - - if (!willRestoreImmediately && !forceOnDemand) { - TabRestoreQueue.add(tab); - } - - this._maybeUpdateBrowserRemoteness({ tabbrowser, tab, - willRestoreImmediately }); - - // Increase the busy state counter before modifying the tab. - this._setWindowStateBusy(window); - - // It's important to set the window state to dirty so that - // we collect their data for the first time when saving state. - DirtyWindows.add(window); - - // In case we didn't collect/receive data for any tabs yet we'll have to - // fill the array with at least empty tabData objects until |_tPos| or - // we'll end up with |null| entries. - for (let otherTab of Array.slice(tabbrowser.tabs, 0, tab._tPos)) { - let emptyState = {entries: [], lastAccessed: otherTab.lastAccessed}; - this._windows[window.__SSi].tabs.push(emptyState); - } - - // Update the tab state in case we shut down without being notified. - this._windows[window.__SSi].tabs[tab._tPos] = tabData; - - // Prepare the tab so that it can be properly restored. We'll pin/unpin - // and show/hide tabs as necessary. We'll also attach a copy of the tab's - // data in case we close it before it's been restored. - if (tabData.pinned) { - tabbrowser.pinTab(tab); - } else { - tabbrowser.unpinTab(tab); - } - - if (tabData.hidden) { - tabbrowser.hideTab(tab); - } else { - tabbrowser.showTab(tab); - } - - if (!!tabData.muted != browser.audioMuted) { - tab.toggleMuteAudio(tabData.muteReason); - } - - if (tabData.lastAccessed) { - tab.updateLastAccessed(tabData.lastAccessed); - } - - if ("attributes" in tabData) { - // Ensure that we persist tab attributes restored from previous sessions. - Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a)); - } - - if (!tabData.entries) { - tabData.entries = []; - } - if (tabData.extData) { - tab.__SS_extdata = Cu.cloneInto(tabData.extData, {}); - } else { - delete tab.__SS_extdata; - } - - // Tab is now open. - delete tabData.closedAt; - - // Ensure the index is in bounds. - let activeIndex = (tabData.index || tabData.entries.length) - 1; - activeIndex = Math.min(activeIndex, tabData.entries.length - 1); - activeIndex = Math.max(activeIndex, 0); - - // Save the index in case we updated it above. - tabData.index = activeIndex + 1; - - // Start a new epoch to discard all frame script messages relating to a - // previous epoch. All async messages that are still on their way to chrome - // will be ignored and don't override any tab data set when restoring. - let epoch = this.startNextEpoch(browser); - - // keep the data around to prevent dataloss in case - // a tab gets closed before it's been properly restored - browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE; - browser.setAttribute("pending", "true"); - tab.setAttribute("pending", "true"); - - // If we're restoring this tab, it certainly shouldn't be in - // the ignored set anymore. - this._crashedBrowsers.delete(browser.permanentKey); - - // Update the persistent tab state cache with |tabData| information. - TabStateCache.update(browser, { - history: {entries: tabData.entries, index: tabData.index}, - scroll: tabData.scroll || null, - storage: tabData.storage || null, - formdata: tabData.formdata || null, - disallow: tabData.disallow || null, - pageStyle: tabData.pageStyle || null, - - // This information is only needed until the tab has finished restoring. - // When that's done it will be removed from the cache and we always - // collect it in TabState._collectBaseTabData(). - image: tabData.image || "", - iconLoadingPrincipal: tabData.iconLoadingPrincipal || null, - userTypedValue: tabData.userTypedValue || "", - userTypedClear: tabData.userTypedClear || 0 - }); - - browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory", - {tabData: tabData, epoch: epoch, loadArguments}); - - // Restore tab attributes. - if ("attributes" in tabData) { - TabAttributes.set(tab, tabData.attributes); - } - - // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but - // it ensures each window will have its selected tab loaded. - if (willRestoreImmediately) { - this.restoreTabContent(tab, loadArguments, reloadInFreshProcess); - } else if (!forceOnDemand) { - this.restoreNextTab(); - } - - // Decrease the busy state counter after we're done. - this._setWindowStateReady(window); - }, - - /** - * Kicks off restoring the given tab. - * - * @param aTab - * the tab to restore - * @param aLoadArguments - * optional load arguments used for loadURI() - * @param aReloadInFreshProcess - * true if we want to reload into a fresh process - */ - restoreTabContent: function (aTab, aLoadArguments = null, aReloadInFreshProcess = false) { - if (aTab.hasAttribute("customizemode") && !aLoadArguments) { - return; - } - - let browser = aTab.linkedBrowser; - let window = aTab.ownerGlobal; - let tabbrowser = window.gBrowser; - let tabData = TabState.clone(aTab); - let activeIndex = tabData.index - 1; - let activePageData = tabData.entries[activeIndex] || null; - let uri = activePageData ? activePageData.url || null : null; - if (aLoadArguments) { - uri = aLoadArguments.uri; - if (aLoadArguments.userContextId) { - browser.setAttribute("usercontextid", aLoadArguments.userContextId); - } - } - - // We have to mark this tab as restoring first, otherwise - // the "pending" attribute will be applied to the linked - // browser, which removes it from the display list. We cannot - // flip the remoteness of any browser that is not being displayed. - this.markTabAsRestoring(aTab); - - let isRemotenessUpdate = false; - if (aReloadInFreshProcess) { - isRemotenessUpdate = tabbrowser.switchBrowserIntoFreshProcess(browser); - } else { - isRemotenessUpdate = tabbrowser.updateBrowserRemotenessByURL(browser, uri); - } - - if (isRemotenessUpdate) { - // We updated the remoteness, so we need to send the history down again. - // - // Start a new epoch to discard all frame script messages relating to a - // previous epoch. All async messages that are still on their way to chrome - // will be ignored and don't override any tab data set when restoring. - let epoch = this.startNextEpoch(browser); - - browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory", { - tabData: tabData, - epoch: epoch, - loadArguments: aLoadArguments, - isRemotenessUpdate, - }); - - } - - // If the restored browser wants to show view source content, start up a - // view source browser that will load the required frame script. - if (uri && ViewSourceBrowser.isViewSource(uri)) { - new ViewSourceBrowser(browser); - } - - browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent", - {loadArguments: aLoadArguments, isRemotenessUpdate}); - }, - - /** - * Marks a given pending tab as restoring. - * - * @param aTab - * the pending tab to mark as restoring - */ - markTabAsRestoring(aTab) { - let browser = aTab.linkedBrowser; - if (browser.__SS_restoreState != TAB_STATE_NEEDS_RESTORE) { - throw new Error("Given tab is not pending."); - } - - // Make sure that this tab is removed from the priority queue. - TabRestoreQueue.remove(aTab); - - // Increase our internal count. - this._tabsRestoringCount++; - - // Set this tab's state to restoring - browser.__SS_restoreState = TAB_STATE_RESTORING; - browser.removeAttribute("pending"); - aTab.removeAttribute("pending"); - }, - - /** - * This _attempts_ to restore the next available tab. If the restore fails, - * then we will attempt the next one. - * There are conditions where this won't do anything: - * if we're in the process of quitting - * if there are no tabs to restore - * if we have already reached the limit for number of tabs to restore - */ - restoreNextTab: function ssi_restoreNextTab() { - // If we call in here while quitting, we don't actually want to do anything - if (RunState.isQuitting) - return; - - // Don't exceed the maximum number of concurrent tab restores. - if (this._tabsRestoringCount >= MAX_CONCURRENT_TAB_RESTORES) - return; - - let tab = TabRestoreQueue.shift(); - if (tab) { - this.restoreTabContent(tab); - } - }, - - /** - * Restore visibility and dimension features to a window - * @param aWindow - * Window reference - * @param aWinData - * Object containing session data for the window - */ - restoreWindowFeatures: function ssi_restoreWindowFeatures(aWindow, aWinData) { - var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[]; - WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) { - aWindow[aItem].visible = hidden.indexOf(aItem) == -1; - }); - - if (aWinData.isPopup) { - this._windows[aWindow.__SSi].isPopup = true; - if (aWindow.gURLBar) { - aWindow.gURLBar.readOnly = true; - aWindow.gURLBar.setAttribute("enablehistory", "false"); - } - } - else { - delete this._windows[aWindow.__SSi].isPopup; - if (aWindow.gURLBar) { - aWindow.gURLBar.readOnly = false; - aWindow.gURLBar.setAttribute("enablehistory", "true"); - } - } - - var _this = this; - aWindow.setTimeout(function() { - _this.restoreDimensions.apply(_this, [aWindow, - +(aWinData.width || 0), - +(aWinData.height || 0), - "screenX" in aWinData ? +aWinData.screenX : NaN, - "screenY" in aWinData ? +aWinData.screenY : NaN, - aWinData.sizemode || "", aWinData.sidebar || ""]); - }, 0); - }, - - /** - * Restore a window's dimensions - * @param aWidth - * Window width - * @param aHeight - * Window height - * @param aLeft - * Window left - * @param aTop - * Window top - * @param aSizeMode - * Window size mode (eg: maximized) - * @param aSidebar - * Sidebar command - */ - restoreDimensions: function ssi_restoreDimensions(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) { - var win = aWindow; - var _this = this; - function win_(aName) { return _this._getWindowDimension(win, aName); } - - // find available space on the screen where this window is being placed - let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight); - if (screen) { - let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {}; - screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight); - // screenX/Y are based on the origin of the screen's desktop-pixel coordinate space - let screenLeftCss = screenLeft.value; - let screenTopCss = screenTop.value; - // convert screen's device pixel dimensions to CSS px dimensions - screen.GetAvailRect(screenLeft, screenTop, screenWidth, screenHeight); - let cssToDevScale = screen.defaultCSSScaleFactor; - let screenRightCss = screenLeftCss + screenWidth.value / cssToDevScale; - let screenBottomCss = screenTopCss + screenHeight.value / cssToDevScale; - - // Pull the window within the screen's bounds (allowing a little slop - // for windows that may be deliberately placed with their border off-screen - // as when Win10 "snaps" a window to the left/right edge -- bug 1276516). - // First, ensure the left edge is large enough... - if (aLeft < screenLeftCss - SCREEN_EDGE_SLOP) { - aLeft = screenLeftCss; - } - // Then check the resulting right edge, and reduce it if necessary. - let right = aLeft + aWidth; - if (right > screenRightCss + SCREEN_EDGE_SLOP) { - right = screenRightCss; - // See if we can move the left edge leftwards to maintain width. - if (aLeft > screenLeftCss) { - aLeft = Math.max(right - aWidth, screenLeftCss); - } - } - // Finally, update aWidth to account for the adjusted left and right edges. - aWidth = right - aLeft; - - // And do the same in the vertical dimension. - if (aTop < screenTopCss - SCREEN_EDGE_SLOP) { - aTop = screenTopCss; - } - let bottom = aTop + aHeight; - if (bottom > screenBottomCss + SCREEN_EDGE_SLOP) { - bottom = screenBottomCss; - if (aTop > screenTopCss) { - aTop = Math.max(bottom - aHeight, screenTopCss); - } - } - aHeight = bottom - aTop; - } - - // only modify those aspects which aren't correct yet - if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) { - aWindow.moveTo(aLeft, aTop); - } - if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) { - // Don't resize the window if it's currently maximized and we would - // maximize it again shortly after. - if (aSizeMode != "maximized" || win_("sizemode") != "maximized") { - aWindow.resizeTo(aWidth, aHeight); - } - } - if (aSizeMode && win_("sizemode") != aSizeMode) - { - switch (aSizeMode) - { - case "maximized": - aWindow.maximize(); - break; - case "minimized": - aWindow.minimize(); - break; - case "normal": - aWindow.restore(); - break; - } - } - var sidebar = aWindow.document.getElementById("sidebar-box"); - if (sidebar.getAttribute("sidebarcommand") != aSidebar) { - aWindow.SidebarUI.show(aSidebar); - } - // since resizing/moving a window brings it to the foreground, - // we might want to re-focus the last focused window - if (this.windowToFocus) { - this.windowToFocus.focus(); - } - }, - - /* ........ Disk Access .............. */ - - /** - * Save the current session state to disk, after a delay. - * - * @param aWindow (optional) - * Will mark the given window as dirty so that we will recollect its - * data before we start writing. - */ - saveStateDelayed: function (aWindow = null) { - if (aWindow) { - DirtyWindows.add(aWindow); - } - - SessionSaver.runDelayed(); - }, - - /* ........ Auxiliary Functions .............. */ - - /** - * Determines whether or not a tab that is being restored needs - * to have its remoteness flipped first. - * - * @param (object) with the following properties: - * - * tabbrowser (<xul:tabbrowser>): - * The tabbrowser that the browser belongs to. - * - * tab (<xul:tab>): - * The tab being restored - * - * willRestoreImmediately (bool): - * true if the tab is going to have its content - * restored immediately by the caller. - * - */ - _maybeUpdateBrowserRemoteness({ tabbrowser, tab, - willRestoreImmediately }) { - // If the browser we're attempting to restore happens to be - // remote, we need to flip it back to non-remote if it's going - // to go into the pending background tab state. This is to make - // sure that a background tab can't crash if it hasn't yet - // been restored. - // - // Normally, when a window is restored, the tabs that SessionStore - // inserts are non-remote - but the initial browser is, by default, - // remote, so this check and flip covers this case. The other case - // is when window state is overwriting the state of an existing - // window with some remote tabs. - let browser = tab.linkedBrowser; - - // There are two ways that a tab might start restoring its content - // very soon - either the caller is going to restore the content - // immediately, or the TabRestoreQueue is set up so that the tab - // content is going to be restored in the very near future. In - // either case, we don't want to flip remoteness, since the browser - // will soon be loading content. - let willRestore = willRestoreImmediately || - TabRestoreQueue.willRestoreSoon(tab); - - if (browser.isRemoteBrowser && !willRestore) { - tabbrowser.updateBrowserRemoteness(browser, false); - } - }, - - /** - * Update the session start time and send a telemetry measurement - * for the number of days elapsed since the session was started. - * - * @param state - * The session state. - */ - _updateSessionStartTime: function ssi_updateSessionStartTime(state) { - // Attempt to load the session start time from the session state - if (state.session && state.session.startTime) { - this._sessionStartTime = state.session.startTime; - } - }, - - /** - * call a callback for all currently opened browser windows - * (might miss the most recent one) - * @param aFunc - * Callback each window is passed to - */ - _forEachBrowserWindow: function ssi_forEachBrowserWindow(aFunc) { - var windowsEnum = Services.wm.getEnumerator("navigator:browser"); - - while (windowsEnum.hasMoreElements()) { - var window = windowsEnum.getNext(); - if (window.__SSi && !window.closed) { - aFunc.call(this, window); - } - } - }, - - /** - * Returns most recent window - * @returns Window reference - */ - _getMostRecentBrowserWindow: function ssi_getMostRecentBrowserWindow() { - return RecentWindow.getMostRecentBrowserWindow({ allowPopups: true }); - }, - - /** - * Calls onClose for windows that are determined to be closed but aren't - * destroyed yet, which would otherwise cause getBrowserState and - * setBrowserState to treat them as open windows. - */ - _handleClosedWindows: function ssi_handleClosedWindows() { - var windowsEnum = Services.wm.getEnumerator("navigator:browser"); - - while (windowsEnum.hasMoreElements()) { - var window = windowsEnum.getNext(); - if (window.closed) { - this.onClose(window); - } - } - }, - - /** - * open a new browser window for a given session state - * called when restoring a multi-window session - * @param aState - * Object containing session data - */ - _openWindowWithState: function ssi_openWindowWithState(aState) { - var argString = Cc["@mozilla.org/supports-string;1"]. - createInstance(Ci.nsISupportsString); - argString.data = ""; - - // Build feature string - let features = "chrome,dialog=no,macsuppressanimation,all"; - let winState = aState.windows[0]; - WINDOW_ATTRIBUTES.forEach(function(aFeature) { - // Use !isNaN as an easy way to ignore sizemode and check for numbers - if (aFeature in winState && !isNaN(winState[aFeature])) - features += "," + aFeature + "=" + winState[aFeature]; - }); - - if (winState.isPrivate) { - features += ",private"; - } - - var window = - Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"), - "_blank", features, argString); - - do { - var ID = "window" + Math.random(); - } while (ID in this._statesToRestore); - this._statesToRestore[(window.__SS_restoreID = ID)] = aState; - - return window; - }, - - /** - * Whether or not to resume session, if not recovering from a crash. - * @returns bool - */ - _doResumeSession: function ssi_doResumeSession() { - return this._prefBranch.getIntPref("startup.page") == 3 || - this._prefBranch.getBoolPref("sessionstore.resume_session_once"); - }, - - /** - * whether the user wants to load any other page at startup - * (except the homepage) - needed for determining whether to overwrite the current tabs - * C.f.: nsBrowserContentHandler's defaultArgs implementation. - * @returns bool - */ - _isCmdLineEmpty: function ssi_isCmdLineEmpty(aWindow, aState) { - var pinnedOnly = aState.windows && - aState.windows.every(win => - win.tabs.every(tab => tab.pinned)); - - let hasFirstArgument = aWindow.arguments && aWindow.arguments[0]; - if (!pinnedOnly) { - let defaultArgs = Cc["@mozilla.org/browser/clh;1"]. - getService(Ci.nsIBrowserHandler).defaultArgs; - if (aWindow.arguments && - aWindow.arguments[0] && - aWindow.arguments[0] == defaultArgs) - hasFirstArgument = false; - } - - return !hasFirstArgument; - }, - - /** - * on popup windows, the XULWindow's attributes seem not to be set correctly - * we use thus JSDOMWindow attributes for sizemode and normal window attributes - * (and hope for reasonable values when maximized/minimized - since then - * outerWidth/outerHeight aren't the dimensions of the restored window) - * @param aWindow - * Window reference - * @param aAttribute - * String sizemode | width | height | other window attribute - * @returns string - */ - _getWindowDimension: function ssi_getWindowDimension(aWindow, aAttribute) { - if (aAttribute == "sizemode") { - switch (aWindow.windowState) { - case aWindow.STATE_FULLSCREEN: - case aWindow.STATE_MAXIMIZED: - return "maximized"; - case aWindow.STATE_MINIMIZED: - return "minimized"; - default: - return "normal"; - } - } - - var dimension; - switch (aAttribute) { - case "width": - dimension = aWindow.outerWidth; - break; - case "height": - dimension = aWindow.outerHeight; - break; - default: - dimension = aAttribute in aWindow ? aWindow[aAttribute] : ""; - break; - } - - if (aWindow.windowState == aWindow.STATE_NORMAL) { - return dimension; - } - return aWindow.document.documentElement.getAttribute(aAttribute) || dimension; - }, - - /** - * @param aState is a session state - * @param aRecentCrashes is the number of consecutive crashes - * @returns whether a restore page will be needed for the session state - */ - _needsRestorePage: function ssi_needsRestorePage(aState, aRecentCrashes) { - const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000; - - // don't display the page when there's nothing to restore - let winData = aState.windows || null; - if (!winData || winData.length == 0) - return false; - - // don't wrap a single about:sessionrestore page - if (this._hasSingleTabWithURL(winData, "about:sessionrestore") || - this._hasSingleTabWithURL(winData, "about:welcomeback")) { - return false; - } - - // don't automatically restore in Safe Mode - if (Services.appinfo.inSafeMode) - return true; - - let max_resumed_crashes = - this._prefBranch.getIntPref("sessionstore.max_resumed_crashes"); - let sessionAge = aState.session && aState.session.lastUpdate && - (Date.now() - aState.session.lastUpdate); - - return max_resumed_crashes != -1 && - (aRecentCrashes > max_resumed_crashes || - sessionAge && sessionAge >= SIX_HOURS_IN_MS); - }, - - /** - * @param aWinData is the set of windows in session state - * @param aURL is the single URL we're looking for - * @returns whether the window data contains only the single URL passed - */ - _hasSingleTabWithURL: function(aWinData, aURL) { - if (aWinData && - aWinData.length == 1 && - aWinData[0].tabs && - aWinData[0].tabs.length == 1 && - aWinData[0].tabs[0].entries && - aWinData[0].tabs[0].entries.length == 1) { - return aURL == aWinData[0].tabs[0].entries[0].url; - } - return false; - }, - - /** - * Determine if the tab state we're passed is something we should save. This - * is used when closing a tab or closing a window with a single tab - * - * @param aTabState - * The current tab state - * @returns boolean - */ - _shouldSaveTabState: function ssi_shouldSaveTabState(aTabState) { - // If the tab has only a transient about: history entry, no other - // session history, and no userTypedValue, then we don't actually want to - // store this tab's data. - return aTabState.entries.length && - !(aTabState.entries.length == 1 && - (aTabState.entries[0].url == "about:blank" || - aTabState.entries[0].url == "about:newtab" || - aTabState.entries[0].url == "about:privatebrowsing") && - !aTabState.userTypedValue); - }, - - /** - * This is going to take a state as provided at startup (via - * nsISessionStartup.state) and split it into 2 parts. The first part - * (defaultState) will be a state that should still be restored at startup, - * while the second part (state) is a state that should be saved for later. - * defaultState will be comprised of windows with only pinned tabs, extracted - * from state. It will contain the cookies that go along with the history - * entries in those tabs. It will also contain window position information. - * - * defaultState will be restored at startup. state will be passed into - * LastSession and will be kept in case the user explicitly wants - * to restore the previous session (publicly exposed as restoreLastSession). - * - * @param state - * The state, presumably from nsISessionStartup.state - * @returns [defaultState, state] - */ - _prepDataForDeferredRestore: function ssi_prepDataForDeferredRestore(state) { - // Make sure that we don't modify the global state as provided by - // nsSessionStartup.state. - state = Cu.cloneInto(state, {}); - - let defaultState = { windows: [], selectedWindow: 1 }; - - state.selectedWindow = state.selectedWindow || 1; - - // Look at each window, remove pinned tabs, adjust selectedindex, - // remove window if necessary. - for (let wIndex = 0; wIndex < state.windows.length;) { - let window = state.windows[wIndex]; - window.selected = window.selected || 1; - // We're going to put the state of the window into this object - let pinnedWindowState = { tabs: [], cookies: []}; - for (let tIndex = 0; tIndex < window.tabs.length;) { - if (window.tabs[tIndex].pinned) { - // Adjust window.selected - if (tIndex + 1 < window.selected) - window.selected -= 1; - else if (tIndex + 1 == window.selected) - pinnedWindowState.selected = pinnedWindowState.tabs.length + 2; - // + 2 because the tab isn't actually in the array yet - - // Now add the pinned tab to our window - pinnedWindowState.tabs = - pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1)); - // We don't want to increment tIndex here. - continue; - } - tIndex++; - } - - // At this point the window in the state object has been modified (or not) - // We want to build the rest of this new window object if we have pinnedTabs. - if (pinnedWindowState.tabs.length) { - // First get the other attributes off the window - WINDOW_ATTRIBUTES.forEach(function(attr) { - if (attr in window) { - pinnedWindowState[attr] = window[attr]; - delete window[attr]; - } - }); - // We're just copying position data into the pinned window. - // Not copying over: - // - _closedTabs - // - extData - // - isPopup - // - hidden - - // Assign a unique ID to correlate the window to be opened with the - // remaining data - window.__lastSessionWindowID = pinnedWindowState.__lastSessionWindowID - = "" + Date.now() + Math.random(); - - // Extract the cookies that belong with each pinned tab - this._splitCookiesFromWindow(window, pinnedWindowState); - - // Actually add this window to our defaultState - defaultState.windows.push(pinnedWindowState); - // Remove the window from the state if it doesn't have any tabs - if (!window.tabs.length) { - if (wIndex + 1 <= state.selectedWindow) - state.selectedWindow -= 1; - else if (wIndex + 1 == state.selectedWindow) - defaultState.selectedIndex = defaultState.windows.length + 1; - - state.windows.splice(wIndex, 1); - // We don't want to increment wIndex here. - continue; - } - - - } - wIndex++; - } - - return [defaultState, state]; - }, - - /** - * Splits out the cookies from aWinState into aTargetWinState based on the - * tabs that are in aTargetWinState. - * This alters the state of aWinState and aTargetWinState. - */ - _splitCookiesFromWindow: - function ssi_splitCookiesFromWindow(aWinState, aTargetWinState) { - if (!aWinState.cookies || !aWinState.cookies.length) - return; - - // Get the hosts for history entries in aTargetWinState - let cookieHosts = SessionCookies.getHostsForWindow(aTargetWinState); - - // By creating a regex we reduce overhead and there is only one loop pass - // through either array (cookieHosts and aWinState.cookies). - let hosts = Object.keys(cookieHosts).join("|").replace(/\./g, "\\."); - // If we don't actually have any hosts, then we don't want to do anything. - if (!hosts.length) - return; - let cookieRegex = new RegExp(".*(" + hosts + ")"); - for (let cIndex = 0; cIndex < aWinState.cookies.length;) { - if (cookieRegex.test(aWinState.cookies[cIndex].host)) { - aTargetWinState.cookies = - aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1)); - continue; - } - cIndex++; - } - }, - - _sendRestoreCompletedNotifications: function ssi_sendRestoreCompletedNotifications() { - // not all windows restored, yet - if (this._restoreCount > 1) { - this._restoreCount--; - return; - } - - // observers were already notified - if (this._restoreCount == -1) - return; - - // This was the last window restored at startup, notify observers. - Services.obs.notifyObservers(null, - this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED, - ""); - - this._browserSetState = false; - this._restoreCount = -1; - }, - - /** - * Set the given window's busy state - * @param aWindow the window - * @param aValue the window's busy state - */ - _setWindowStateBusyValue: - function ssi_changeWindowStateBusyValue(aWindow, aValue) { - - this._windows[aWindow.__SSi].busy = aValue; - - // Keep the to-be-restored state in sync because that is returned by - // getWindowState() as long as the window isn't loaded, yet. - if (!this._isWindowLoaded(aWindow)) { - let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0]; - stateToRestore.busy = aValue; - } - }, - - /** - * Set the given window's state to 'not busy'. - * @param aWindow the window - */ - _setWindowStateReady: function ssi_setWindowStateReady(aWindow) { - let newCount = (this._windowBusyStates.get(aWindow) || 0) - 1; - if (newCount < 0) { - throw new Error("Invalid window busy state (less than zero)."); - } - this._windowBusyStates.set(aWindow, newCount); - - if (newCount == 0) { - this._setWindowStateBusyValue(aWindow, false); - this._sendWindowStateEvent(aWindow, "Ready"); - } - }, - - /** - * Set the given window's state to 'busy'. - * @param aWindow the window - */ - _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) { - let newCount = (this._windowBusyStates.get(aWindow) || 0) + 1; - this._windowBusyStates.set(aWindow, newCount); - - if (newCount == 1) { - this._setWindowStateBusyValue(aWindow, true); - this._sendWindowStateEvent(aWindow, "Busy"); - } - }, - - /** - * Dispatch an SSWindowState_____ event for the given window. - * @param aWindow the window - * @param aType the type of event, SSWindowState will be prepended to this string - */ - _sendWindowStateEvent: function ssi_sendWindowStateEvent(aWindow, aType) { - let event = aWindow.document.createEvent("Events"); - event.initEvent("SSWindowState" + aType, true, false); - aWindow.dispatchEvent(event); - }, - - /** - * Dispatch the SSWindowRestored event for the given window. - * @param aWindow - * The window which has been restored - */ - _sendWindowRestoredNotification(aWindow) { - let event = aWindow.document.createEvent("Events"); - event.initEvent("SSWindowRestored", true, false); - aWindow.dispatchEvent(event); - }, - - /** - * Dispatch the SSTabRestored event for the given tab. - * @param aTab - * The tab which has been restored - * @param aIsRemotenessUpdate - * True if this tab was restored due to flip from running from - * out-of-main-process to in-main-process or vice-versa. - */ - _sendTabRestoredNotification(aTab, aIsRemotenessUpdate) { - let event = aTab.ownerDocument.createEvent("CustomEvent"); - event.initCustomEvent("SSTabRestored", true, false, { - isRemotenessUpdate: aIsRemotenessUpdate, - }); - aTab.dispatchEvent(event); - }, - - /** - * @param aWindow - * Window reference - * @returns whether this window's data is still cached in _statesToRestore - * because it's not fully loaded yet - */ - _isWindowLoaded: function ssi_isWindowLoaded(aWindow) { - return !aWindow.__SS_restoreID; - }, - - /** - * Replace "Loading..." with the tab label (with minimal side-effects) - * @param aString is the string the title is stored in - * @param aTabbrowser is a tabbrowser object, containing aTab - * @param aTab is the tab whose title we're updating & using - * - * @returns aString that has been updated with the new title - */ - _replaceLoadingTitle : function ssi_replaceLoadingTitle(aString, aTabbrowser, aTab) { - if (aString == aTabbrowser.mStringBundle.getString("tabs.connecting")) { - aTabbrowser.setTabTitle(aTab); - [aString, aTab.label] = [aTab.label, aString]; - } - return aString; - }, - - /** - * Resize this._closedWindows to the value of the pref, except in the case - * where we don't have any non-popup windows on Windows and Linux. Then we must - * resize such that we have at least one non-popup window. - */ - _capClosedWindows : function ssi_capClosedWindows() { - if (this._closedWindows.length <= this._max_windows_undo) - return; - let spliceTo = this._max_windows_undo; - if (AppConstants.platform != "macosx") { - let normalWindowIndex = 0; - // try to find a non-popup window in this._closedWindows - while (normalWindowIndex < this._closedWindows.length && - !!this._closedWindows[normalWindowIndex].isPopup) - normalWindowIndex++; - if (normalWindowIndex >= this._max_windows_undo) - spliceTo = normalWindowIndex + 1; - } - this._closedWindows.splice(spliceTo, this._closedWindows.length); - }, - - /** - * Clears the set of windows that are "resurrected" before writing to disk to - * make closing windows one after the other until shutdown work as expected. - * - * This function should only be called when we are sure that there has been - * a user action that indicates the browser is actively being used and all - * windows that have been closed before are not part of a series of closing - * windows. - */ - _clearRestoringWindows: function ssi_clearRestoringWindows() { - for (let i = 0; i < this._closedWindows.length; i++) { - delete this._closedWindows[i]._shouldRestore; - } - }, - - /** - * Reset state to prepare for a new session state to be restored. - */ - _resetRestoringState: function ssi_initRestoringState() { - TabRestoreQueue.reset(); - this._tabsRestoringCount = 0; - }, - - /** - * Reset the restoring state for a particular tab. This will be called when - * removing a tab or when a tab needs to be reset (it's being overwritten). - * - * @param aTab - * The tab that will be "reset" - */ - _resetLocalTabRestoringState: function (aTab) { - NS_ASSERT(aTab.linkedBrowser.__SS_restoreState, - "given tab is not restoring"); - - let browser = aTab.linkedBrowser; - - // Keep the tab's previous state for later in this method - let previousState = browser.__SS_restoreState; - - // The browser is no longer in any sort of restoring state. - delete browser.__SS_restoreState; - - aTab.removeAttribute("pending"); - browser.removeAttribute("pending"); - - if (previousState == TAB_STATE_RESTORING) { - if (this._tabsRestoringCount) - this._tabsRestoringCount--; - } else if (previousState == TAB_STATE_NEEDS_RESTORE) { - // Make sure that the tab is removed from the list of tabs to restore. - // Again, this is normally done in restoreTabContent, but that isn't being called - // for this tab. - TabRestoreQueue.remove(aTab); - } - }, - - _resetTabRestoringState: function (tab) { - NS_ASSERT(tab.linkedBrowser.__SS_restoreState, - "given tab is not restoring"); - - let browser = tab.linkedBrowser; - browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {}); - this._resetLocalTabRestoringState(tab); - }, - - /** - * Each fresh tab starts out with epoch=0. This function can be used to - * start a next epoch by incrementing the current value. It will enables us - * to ignore stale messages sent from previous epochs. The function returns - * the new epoch ID for the given |browser|. - */ - startNextEpoch(browser) { - let next = this.getCurrentEpoch(browser) + 1; - this._browserEpochs.set(browser.permanentKey, next); - return next; - }, - - /** - * Returns the current epoch for the given <browser>. If we haven't assigned - * a new epoch this will default to zero for new tabs. - */ - getCurrentEpoch(browser) { - return this._browserEpochs.get(browser.permanentKey) || 0; - }, - - /** - * Each time a <browser> element is restored, we increment its "epoch". To - * check if a message from content-sessionStore.js is out of date, we can - * compare the epoch received with the message to the <browser> element's - * epoch. This function does that, and returns true if |epoch| is up-to-date - * with respect to |browser|. - */ - isCurrentEpoch: function (browser, epoch) { - return this.getCurrentEpoch(browser) == epoch; - }, - - /** - * Resets the epoch for a given <browser>. We need to this every time we - * receive a hint that a new docShell has been loaded into the browser as - * the frame script starts out with epoch=0. - */ - resetEpoch(browser) { - this._browserEpochs.delete(browser.permanentKey); - }, - - /** - * Handle an error report from a content process. - */ - reportInternalError(data) { - // For the moment, we only report errors through Telemetry. - if (data.telemetry) { - for (let key of Object.keys(data.telemetry)) { - let histogram = Telemetry.getHistogramById(key); - histogram.add(data.telemetry[key]); - } - } - }, - - /** - * Countdown for a given duration, skipping beats if the computer is too busy, - * sleeping or otherwise unavailable. - * - * @param {number} delay An approximate delay to wait in milliseconds (rounded - * up to the closest second). - * - * @return Promise - */ - looseTimer(delay) { - let DELAY_BEAT = 1000; - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let beats = Math.ceil(delay / DELAY_BEAT); - let promise = new Promise(resolve => { - timer.initWithCallback(function() { - if (beats <= 0) { - resolve(); - } - --beats; - }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP); - }); - // Ensure that the timer is both canceled once we are done with it - // and not garbage-collected until then. - promise.then(() => timer.cancel(), () => timer.cancel()); - return promise; - } -}; - -/** - * Priority queue that keeps track of a list of tabs to restore and returns - * the tab we should restore next, based on priority rules. We decide between - * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only - * restored with restore_hidden_tabs=true. - */ -var TabRestoreQueue = { - // The separate buckets used to store tabs. - tabs: {priority: [], visible: [], hidden: []}, - - // Preferences used by the TabRestoreQueue to determine which tabs - // are restored automatically and which tabs will be on-demand. - prefs: { - // Lazy getter that returns whether tabs are restored on demand. - get restoreOnDemand() { - let updateValue = () => { - let value = Services.prefs.getBoolPref(PREF); - let definition = {value: value, configurable: true}; - Object.defineProperty(this, "restoreOnDemand", definition); - return value; - } - - const PREF = "browser.sessionstore.restore_on_demand"; - Services.prefs.addObserver(PREF, updateValue, false); - return updateValue(); - }, - - // Lazy getter that returns whether pinned tabs are restored on demand. - get restorePinnedTabsOnDemand() { - let updateValue = () => { - let value = Services.prefs.getBoolPref(PREF); - let definition = {value: value, configurable: true}; - Object.defineProperty(this, "restorePinnedTabsOnDemand", definition); - return value; - } - - const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand"; - Services.prefs.addObserver(PREF, updateValue, false); - return updateValue(); - }, - - // Lazy getter that returns whether we should restore hidden tabs. - get restoreHiddenTabs() { - let updateValue = () => { - let value = Services.prefs.getBoolPref(PREF); - let definition = {value: value, configurable: true}; - Object.defineProperty(this, "restoreHiddenTabs", definition); - return value; - } - - const PREF = "browser.sessionstore.restore_hidden_tabs"; - Services.prefs.addObserver(PREF, updateValue, false); - return updateValue(); - } - }, - - // Resets the queue and removes all tabs. - reset: function () { - this.tabs = {priority: [], visible: [], hidden: []}; - }, - - // Adds a tab to the queue and determines its priority bucket. - add: function (tab) { - let {priority, hidden, visible} = this.tabs; - - if (tab.pinned) { - priority.push(tab); - } else if (tab.hidden) { - hidden.push(tab); - } else { - visible.push(tab); - } - }, - - // Removes a given tab from the queue, if it's in there. - remove: function (tab) { - let {priority, hidden, visible} = this.tabs; - - // We'll always check priority first since we don't - // have an indicator if a tab will be there or not. - let set = priority; - let index = set.indexOf(tab); - - if (index == -1) { - set = tab.hidden ? hidden : visible; - index = set.indexOf(tab); - } - - if (index > -1) { - set.splice(index, 1); - } - }, - - // Returns and removes the tab with the highest priority. - shift: function () { - let set; - let {priority, hidden, visible} = this.tabs; - - let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs; - let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand); - if (restorePinned && priority.length) { - set = priority; - } else if (!restoreOnDemand) { - if (visible.length) { - set = visible; - } else if (this.prefs.restoreHiddenTabs && hidden.length) { - set = hidden; - } - } - - return set && set.shift(); - }, - - // Moves a given tab from the 'hidden' to the 'visible' bucket. - hiddenToVisible: function (tab) { - let {hidden, visible} = this.tabs; - let index = hidden.indexOf(tab); - - if (index > -1) { - hidden.splice(index, 1); - visible.push(tab); - } - }, - - // Moves a given tab from the 'visible' to the 'hidden' bucket. - visibleToHidden: function (tab) { - let {visible, hidden} = this.tabs; - let index = visible.indexOf(tab); - - if (index > -1) { - visible.splice(index, 1); - hidden.push(tab); - } - }, - - /** - * Returns true if the passed tab is in one of the sets that we're - * restoring content in automatically. - * - * @param tab (<xul:tab>) - * The tab to check - * @returns bool - */ - willRestoreSoon: function (tab) { - let { priority, hidden, visible } = this.tabs; - let { restoreOnDemand, restorePinnedTabsOnDemand, - restoreHiddenTabs } = this.prefs; - let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand); - let candidateSet = []; - - if (restorePinned && priority.length) - candidateSet.push(...priority); - - if (!restoreOnDemand) { - if (visible.length) - candidateSet.push(...visible); - - if (restoreHiddenTabs && hidden.length) - candidateSet.push(...hidden); - } - - return candidateSet.indexOf(tab) > -1; - }, -}; - -// A map storing a closed window's state data until it goes aways (is GC'ed). -// This ensures that API clients can still read (but not write) states of -// windows they still hold a reference to but we don't. -var DyingWindowCache = { - _data: new WeakMap(), - - has: function (window) { - return this._data.has(window); - }, - - get: function (window) { - return this._data.get(window); - }, - - set: function (window, data) { - this._data.set(window, data); - }, - - remove: function (window) { - this._data.delete(window); - } -}; - -// A weak set of dirty windows. We use it to determine which windows we need to -// recollect data for when getCurrentState() is called. -var DirtyWindows = { - _data: new WeakMap(), - - has: function (window) { - return this._data.has(window); - }, - - add: function (window) { - return this._data.set(window, true); - }, - - remove: function (window) { - this._data.delete(window); - }, - - clear: function (window) { - this._data = new WeakMap(); - } -}; - -// The state from the previous session (after restoring pinned tabs). This -// state is persisted and passed through to the next session during an app -// restart to make the third party add-on warning not trash the deferred -// session -var LastSession = { - _state: null, - - get canRestore() { - return !!this._state; - }, - - getState: function () { - return this._state; - }, - - setState: function (state) { - this._state = state; - }, - - clear: function () { - if (this._state) { - this._state = null; - Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null); - } - } -}; diff --git a/browser/components/sessionstore/SessionWorker.js b/browser/components/sessionstore/SessionWorker.js deleted file mode 100644 index 7d802a7df..000000000 --- a/browser/components/sessionstore/SessionWorker.js +++ /dev/null @@ -1,381 +0,0 @@ -/* 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/. */ - -/** - * A worker dedicated to handle I/O for Session Store. - */ - -"use strict"; - -importScripts("resource://gre/modules/osfile.jsm"); - -var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js"); - -var File = OS.File; -var Encoder = new TextEncoder(); -var Decoder = new TextDecoder(); - -var worker = new PromiseWorker.AbstractWorker(); -worker.dispatch = function(method, args = []) { - return Agent[method](...args); -}; -worker.postMessage = function(result, ...transfers) { - self.postMessage(result, ...transfers); -}; -worker.close = function() { - self.close(); -}; - -self.addEventListener("message", msg => worker.handleMessage(msg)); - -// The various possible states - -/** - * We just started (we haven't written anything to disk yet) from - * `Paths.clean`. The backup directory may not exist. - */ -const STATE_CLEAN = "clean"; -/** - * We know that `Paths.recovery` is good, either because we just read - * it (we haven't written anything to disk yet) or because have - * already written once to `Paths.recovery` during this session. - * `Paths.clean` is absent or invalid. The backup directory exists. - */ -const STATE_RECOVERY = "recovery"; -/** - * We just started from `Paths.recoverBackupy` (we haven't written - * anything to disk yet). Both `Paths.clean` and `Paths.recovery` are - * absent or invalid. The backup directory exists. - */ -const STATE_RECOVERY_BACKUP = "recoveryBackup"; -/** - * We just started from `Paths.upgradeBackup` (we haven't written - * anything to disk yet). Both `Paths.clean`, `Paths.recovery` and - * `Paths.recoveryBackup` are absent or invalid. The backup directory - * exists. - */ -const STATE_UPGRADE_BACKUP = "upgradeBackup"; -/** - * We just started without a valid session store file (we haven't - * written anything to disk yet). The backup directory may not exist. - */ -const STATE_EMPTY = "empty"; - -var Agent = { - // Path to the files used by the SessionWorker - Paths: null, - - /** - * The current state of the worker, as one of the following strings: - * - "permanent", once the first write has been completed; - * - "empty", before the first write has been completed, - * if we have started without any sessionstore; - * - one of "clean", "recovery", "recoveryBackup", "cleanBackup", - * "upgradeBackup", before the first write has been completed, if - * we have started by loading the corresponding file. - */ - state: null, - - /** - * Number of old upgrade backups that are being kept - */ - maxUpgradeBackups: null, - - /** - * Initialize (or reinitialize) the worker - * - * @param {string} origin Which of sessionstore.js or its backups - * was used. One of the `STATE_*` constants defined above. - * @param {object} paths The paths at which to find the various files. - * @param {object} prefs The preferences the worker needs to known. - */ - init(origin, paths, prefs = {}) { - if (!(origin in paths || origin == STATE_EMPTY)) { - throw new TypeError("Invalid origin: " + origin); - } - - // Check that all required preference values were passed. - for (let pref of ["maxUpgradeBackups", "maxSerializeBack", "maxSerializeForward"]) { - if (!prefs.hasOwnProperty(pref)) { - throw new TypeError(`Missing preference value for ${pref}`); - } - } - - this.state = origin; - this.Paths = paths; - this.maxUpgradeBackups = prefs.maxUpgradeBackups; - this.maxSerializeBack = prefs.maxSerializeBack; - this.maxSerializeForward = prefs.maxSerializeForward; - this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup; - return {result: true}; - }, - - /** - * Write the session to disk. - * Write the session to disk, performing any necessary backup - * along the way. - * - * @param {object} state The state to write to disk. - * @param {object} options - * - performShutdownCleanup If |true|, we should - * perform shutdown-time cleanup to ensure that private data - * is not left lying around; - * - isFinalWrite If |true|, write to Paths.clean instead of - * Paths.recovery - */ - write: function (state, options = {}) { - let exn; - let telemetry = {}; - - // Cap the number of backward and forward shistory entries on shutdown. - if (options.isFinalWrite) { - for (let window of state.windows) { - for (let tab of window.tabs) { - let lower = 0; - let upper = tab.entries.length; - - if (this.maxSerializeBack > -1) { - lower = Math.max(lower, tab.index - this.maxSerializeBack - 1); - } - if (this.maxSerializeForward > -1) { - upper = Math.min(upper, tab.index + this.maxSerializeForward); - } - - tab.entries = tab.entries.slice(lower, upper); - tab.index -= lower; - } - } - } - - let stateString = JSON.stringify(state); - let data = Encoder.encode(stateString); - - try { - - if (this.state == STATE_CLEAN || this.state == STATE_EMPTY) { - // The backups directory may not exist yet. In all other cases, - // we have either already read from or already written to this - // directory, so we are satisfied that it exists. - File.makeDir(this.Paths.backups); - } - - if (this.state == STATE_CLEAN) { - // Move $Path.clean out of the way, to avoid any ambiguity as - // to which file is more recent. - File.move(this.Paths.clean, this.Paths.cleanBackup); - } - - let startWriteMs = Date.now(); - - if (options.isFinalWrite) { - // We are shutting down. At this stage, we know that - // $Paths.clean is either absent or corrupted. If it was - // originally present and valid, it has been moved to - // $Paths.cleanBackup a long time ago. We can therefore write - // with the guarantees that we erase no important data. - File.writeAtomic(this.Paths.clean, data, { - tmpPath: this.Paths.clean + ".tmp" - }); - } else if (this.state == STATE_RECOVERY) { - // At this stage, either $Paths.recovery was written >= 15 - // seconds ago during this session or we have just started - // from $Paths.recovery left from the previous session. Either - // way, $Paths.recovery is good. We can move $Path.backup to - // $Path.recoveryBackup without erasing a good file with a bad - // file. - File.writeAtomic(this.Paths.recovery, data, { - tmpPath: this.Paths.recovery + ".tmp", - backupTo: this.Paths.recoveryBackup - }); - } else { - // In other cases, either $Path.recovery is not necessary, or - // it doesn't exist or it has been corrupted. Regardless, - // don't backup $Path.recovery. - File.writeAtomic(this.Paths.recovery, data, { - tmpPath: this.Paths.recovery + ".tmp" - }); - } - - telemetry.FX_SESSION_RESTORE_WRITE_FILE_MS = Date.now() - startWriteMs; - telemetry.FX_SESSION_RESTORE_FILE_SIZE_BYTES = data.byteLength; - - } catch (ex) { - // Don't throw immediately - exn = exn || ex; - } - - // If necessary, perform an upgrade backup - let upgradeBackupComplete = false; - if (this.upgradeBackupNeeded - && (this.state == STATE_CLEAN || this.state == STATE_UPGRADE_BACKUP)) { - try { - // If we loaded from `clean`, the file has since then been renamed to `cleanBackup`. - let path = this.state == STATE_CLEAN ? this.Paths.cleanBackup : this.Paths.upgradeBackup; - File.copy(path, this.Paths.nextUpgradeBackup); - this.upgradeBackupNeeded = false; - upgradeBackupComplete = true; - } catch (ex) { - // Don't throw immediately - exn = exn || ex; - } - - // Find all backups - let iterator; - let backups = []; // array that will contain the paths to all upgrade backup - let upgradeBackupPrefix = this.Paths.upgradeBackupPrefix; // access for forEach callback - - try { - iterator = new File.DirectoryIterator(this.Paths.backups); - iterator.forEach(function (file) { - if (file.path.startsWith(upgradeBackupPrefix)) { - backups.push(file.path); - } - }, this); - } catch (ex) { - // Don't throw immediately - exn = exn || ex; - } finally { - if (iterator) { - iterator.close(); - } - } - - // If too many backups exist, delete them - if (backups.length > this.maxUpgradeBackups) { - // Use alphanumerical sort since dates are in YYYYMMDDHHMMSS format - backups.sort().forEach((file, i) => { - // remove backup file if it is among the first (n-maxUpgradeBackups) files - if (i < backups.length - this.maxUpgradeBackups) { - File.remove(file); - } - }); - } - } - - if (options.performShutdownCleanup && !exn) { - - // During shutdown, if auto-restore is disabled, we need to - // remove possibly sensitive data that has been stored purely - // for crash recovery. Note that this slightly decreases our - // ability to recover from OS-level/hardware-level issue. - - // If an exception was raised, we assume that we still need - // these files. - File.remove(this.Paths.recoveryBackup); - File.remove(this.Paths.recovery); - } - - this.state = STATE_RECOVERY; - - if (exn) { - throw exn; - } - - return { - result: { - upgradeBackup: upgradeBackupComplete - }, - telemetry: telemetry, - }; - }, - - /** - * Wipes all files holding session data from disk. - */ - wipe: function () { - - // Don't stop immediately in case of error. - let exn = null; - - // Erase main session state file - try { - File.remove(this.Paths.clean); - } catch (ex) { - // Don't stop immediately. - exn = exn || ex; - } - - // Wipe the Session Restore directory - try { - this._wipeFromDir(this.Paths.backups, null); - } catch (ex) { - exn = exn || ex; - } - - try { - File.removeDir(this.Paths.backups); - } catch (ex) { - exn = exn || ex; - } - - // Wipe legacy Ression Restore files from the profile directory - try { - this._wipeFromDir(OS.Constants.Path.profileDir, "sessionstore.bak"); - } catch (ex) { - exn = exn || ex; - } - - - this.state = STATE_EMPTY; - if (exn) { - throw exn; - } - - return { result: true }; - }, - - /** - * Wipe a number of files from a directory. - * - * @param {string} path The directory. - * @param {string|null} prefix If provided, only remove files whose - * name starts with a specific prefix. - */ - _wipeFromDir: function(path, prefix) { - // Sanity check - if (typeof prefix == "undefined" || prefix == "") { - throw new TypeError(); - } - - let exn = null; - - let iterator = new File.DirectoryIterator(path); - try { - if (!iterator.exists()) { - return; - } - for (let entry in iterator) { - if (entry.isDir) { - continue; - } - if (!prefix || entry.name.startsWith(prefix)) { - try { - File.remove(entry.path); - } catch (ex) { - // Don't stop immediately - exn = exn || ex; - } - } - } - - if (exn) { - throw exn; - } - } finally { - iterator.close(); - } - }, -}; - -function isNoSuchFileEx(aReason) { - return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; -} - -/** - * Estimate the number of bytes that a data structure will use on disk - * once serialized. - */ -function getByteLength(str) { - return Encoder.encode(JSON.stringify(str)).byteLength; -} diff --git a/browser/components/sessionstore/SessionWorker.jsm b/browser/components/sessionstore/SessionWorker.jsm deleted file mode 100644 index b26e531ac..000000000 --- a/browser/components/sessionstore/SessionWorker.jsm +++ /dev/null @@ -1,25 +0,0 @@ -/* 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"; - -/** - * Interface to a dedicated thread handling I/O - */ - -const Cu = Components.utils; -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; - -Cu.import("resource://gre/modules/PromiseWorker.jsm", this); -Cu.import("resource://gre/modules/osfile.jsm", this); - -this.EXPORTED_SYMBOLS = ["SessionWorker"]; - -this.SessionWorker = new BasePromiseWorker("resource:///modules/sessionstore/SessionWorker.js"); -// As the Session Worker performs I/O, we can receive instances of -// OS.File.Error, so we need to install a decoder. -this.SessionWorker.ExceptionHandlers["OS.File.Error"] = OS.File.Error.fromMsg; - diff --git a/browser/components/sessionstore/StartupPerformance.jsm b/browser/components/sessionstore/StartupPerformance.jsm deleted file mode 100644 index d1b77a237..000000000 --- a/browser/components/sessionstore/StartupPerformance.jsm +++ /dev/null @@ -1,234 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["StartupPerformance"]; - -const { utils: Cu, classes: Cc, interfaces: Ci } = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); - -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "console", - "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", - "resource://gre/modules/Timer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout", - "resource://gre/modules/Timer.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); - -const COLLECT_RESULTS_AFTER_MS = 10000; - -const OBSERVED_TOPICS = ["sessionstore-restoring-on-startup", "sessionstore-initiating-manual-restore"]; - -this.StartupPerformance = { - /** - * Once we have finished restoring initial tabs, we broadcast on this topic. - */ - RESTORED_TOPIC: "sessionstore-finished-restoring-initial-tabs", - - // Instant at which we have started restoration (notification "sessionstore-restoring-on-startup") - _startTimeStamp: null, - - // Latest instant at which we have finished restoring a tab (DOM event "SSTabRestored") - _latestRestoredTimeStamp: null, - - // A promise resolved once we have finished restoring all the startup tabs. - _promiseFinished: null, - - // Function `resolve()` for `_promiseFinished`. - _resolveFinished: null, - - // A timer - _deadlineTimer: null, - - // `true` once the timer has fired - _hasFired: false, - - // `true` once we are restored - _isRestored: false, - - // Statistics on the session we need to restore. - _totalNumberOfEagerTabs: 0, - _totalNumberOfTabs: 0, - _totalNumberOfWindows: 0, - - init: function() { - for (let topic of OBSERVED_TOPICS) { - Services.obs.addObserver(this, topic, false); - } - }, - - /** - * Return the timestamp at which we finished restoring the latest tab. - * - * This information is not really interesting until we have finished restoring - * tabs. - */ - get latestRestoredTimeStamp() { - return this._latestRestoredTimeStamp; - }, - - /** - * `true` once we have finished restoring startup tabs. - */ - get isRestored() { - return this._isRestored; - }, - - // Called when restoration starts. - // Record the start timestamp, setup the timer and `this._promiseFinished`. - // Behavior is unspecified if there was already an ongoing measure. - _onRestorationStarts: function(isAutoRestore) { - this._latestRestoredTimeStamp = this._startTimeStamp = Date.now(); - this._totalNumberOfEagerTabs = 0; - this._totalNumberOfTabs = 0; - this._totalNumberOfWindows = 0; - - // While we may restore several sessions in a single run of the browser, - // that's a very unusual case, and not really worth measuring, so let's - // stop listening for further restorations. - - for (let topic of OBSERVED_TOPICS) { - Services.obs.removeObserver(this, topic); - } - - Services.obs.addObserver(this, "sessionstore-single-window-restored", false); - this._promiseFinished = new Promise(resolve => { - this._resolveFinished = resolve; - }); - this._promiseFinished.then(() => { - try { - this._isRestored = true; - Services.obs.notifyObservers(null, this.RESTORED_TOPIC, ""); - - if (this._latestRestoredTimeStamp == this._startTimeStamp) { - // Apparently, we haven't restored any tab. - return; - } - - // Once we are done restoring tabs, update Telemetry. - let histogramName = isAutoRestore ? - "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS" : - "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS"; - let histogram = Services.telemetry.getHistogramById(histogramName); - let delta = this._latestRestoredTimeStamp - this._startTimeStamp; - histogram.add(delta); - - Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED").add(this._totalNumberOfEagerTabs); - Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED").add(this._totalNumberOfTabs); - Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED").add(this._totalNumberOfWindows); - - // Reset - this._startTimeStamp = null; - } catch (ex) { - console.error("StartupPerformance: error after resolving promise", ex); - } - }); - }, - - _startTimer: function() { - if (this._hasFired) { - return; - } - if (this._deadlineTimer) { - clearTimeout(this._deadlineTimer); - } - this._deadlineTimer = setTimeout(() => { - try { - this._resolveFinished(); - } catch (ex) { - console.error("StartupPerformance: Error in timeout handler", ex); - } finally { - // Clean up. - this._deadlineTimer = null; - this._hasFired = true; - this._resolveFinished = null; - Services.obs.removeObserver(this, "sessionstore-single-window-restored"); - } - }, COLLECT_RESULTS_AFTER_MS); - }, - - observe: function(subject, topic, details) { - try { - switch (topic) { - case "sessionstore-restoring-on-startup": - this._onRestorationStarts(true); - break; - case "sessionstore-initiating-manual-restore": - this._onRestorationStarts(false); - break; - case "sessionstore-single-window-restored": { - // Session Restore has just opened a window with (initially empty) tabs. - // Some of these tabs will be restored eagerly, while others will be - // restored on demand. The process becomes usable only when all windows - // have finished restored their eager tabs. - // - // While it would be possible to track the restoration of each tab - // from within SessionRestore to determine exactly when the process - // becomes usable, experience shows that this is too invasive. Rather, - // we employ the following heuristic: - // - we maintain a timer of `COLLECT_RESULTS_AFTER_MS` that we expect - // will be triggered only once all tabs have been restored; - // - whenever we restore a new window (hence a bunch of eager tabs), - // we postpone the timer to ensure that the new eager tabs have - // `COLLECT_RESULTS_AFTER_MS` to be restored; - // - whenever a tab is restored, we update - // `this._latestRestoredTimeStamp`; - // - after `COLLECT_RESULTS_AFTER_MS`, we collect the final version - // of `this._latestRestoredTimeStamp`, and use it to determine the - // entire duration of the collection. - // - // Note that this heuristic may be inaccurate if a user clicks - // immediately on a restore-on-demand tab before the end of - // `COLLECT_RESULTS_AFTER_MS`. We assume that this will not - // affect too much the results. - // - // Reset the delay, to give the tabs a little (more) time to restore. - this._startTimer(); - - this._totalNumberOfWindows += 1; - - // Observe the restoration of all tabs. We assume that all tabs of this - // window will have been restored before `COLLECT_RESULTS_AFTER_MS`. - // The last call to `observer` will let us determine how long it took - // to reach that point. - let win = subject; - - let observer = (event) => { - // We don't care about tab restorations that are due to - // a browser flipping from out-of-main-process to in-main-process - // or vice-versa. We only care about restorations that are due - // to the user switching to a lazily restored tab, or for tabs - // that are restoring eagerly. - if (!event.detail.isRemotenessUpdate) { - this._latestRestoredTimeStamp = Date.now(); - this._totalNumberOfEagerTabs += 1; - } - }; - win.gBrowser.tabContainer.addEventListener("SSTabRestored", observer); - this._totalNumberOfTabs += win.gBrowser.tabContainer.itemCount; - - // Once we have finished collecting the results, clean up the observers. - this._promiseFinished.then(() => { - if (!win.gBrowser.tabContainer) { - // May be undefined during shutdown and/or some tests. - return; - } - win.gBrowser.tabContainer.removeEventListener("SSTabRestored", observer); - }); - } - break; - default: - throw new Error(`Unexpected topic ${topic}`); - } - } catch (ex) { - console.error("StartupPerformance error", ex, ex.stack); - throw ex; - } - } -}; diff --git a/browser/components/sessionstore/TabAttributes.jsm b/browser/components/sessionstore/TabAttributes.jsm deleted file mode 100644 index 8a29680f4..000000000 --- a/browser/components/sessionstore/TabAttributes.jsm +++ /dev/null @@ -1,74 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["TabAttributes"]; - -// We never want to directly read or write these attributes. -// 'image' should not be accessed directly but handled by using the -// gBrowser.getIcon()/setIcon() methods. -// 'muted' should not be accessed directly but handled by using the -// tab.linkedBrowser.audioMuted/toggleMuteAudio methods. -// 'pending' is used internal by sessionstore and managed accordingly. -// 'iconLoadingPrincipal' is same as 'image' that it should be handled by -// using the gBrowser.getIcon()/setIcon() methods. -const ATTRIBUTES_TO_SKIP = new Set(["image", "muted", "pending", "iconLoadingPrincipal"]); - -// A set of tab attributes to persist. We will read a given list of tab -// attributes when collecting tab data and will re-set those attributes when -// the given tab data is restored to a new tab. -this.TabAttributes = Object.freeze({ - persist: function (name) { - return TabAttributesInternal.persist(name); - }, - - get: function (tab) { - return TabAttributesInternal.get(tab); - }, - - set: function (tab, data = {}) { - TabAttributesInternal.set(tab, data); - } -}); - -var TabAttributesInternal = { - _attrs: new Set(), - - persist: function (name) { - if (this._attrs.has(name) || ATTRIBUTES_TO_SKIP.has(name)) { - return false; - } - - this._attrs.add(name); - return true; - }, - - get: function (tab) { - let data = {}; - - for (let name of this._attrs) { - if (tab.hasAttribute(name)) { - data[name] = tab.getAttribute(name); - } - } - - return data; - }, - - set: function (tab, data = {}) { - // Clear attributes. - for (let name of this._attrs) { - tab.removeAttribute(name); - } - - // Set attributes. - for (let name in data) { - if (!ATTRIBUTES_TO_SKIP.has(name)) { - tab.setAttribute(name, data[name]); - } - } - } -}; - diff --git a/browser/components/sessionstore/TabState.jsm b/browser/components/sessionstore/TabState.jsm deleted file mode 100644 index f22c52fe3..000000000 --- a/browser/components/sessionstore/TabState.jsm +++ /dev/null @@ -1,196 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["TabState"]; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); - -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter", - "resource:///modules/sessionstore/PrivacyFilter.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache", - "resource:///modules/sessionstore/TabStateCache.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes", - "resource:///modules/sessionstore/TabAttributes.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Utils", - "resource://gre/modules/sessionstore/Utils.jsm"); - -/** - * Module that contains tab state collection methods. - */ -this.TabState = Object.freeze({ - update: function (browser, data) { - TabStateInternal.update(browser, data); - }, - - collect: function (tab) { - return TabStateInternal.collect(tab); - }, - - clone: function (tab) { - return TabStateInternal.clone(tab); - }, - - copyFromCache(browser, tabData, options) { - TabStateInternal.copyFromCache(browser, tabData, options); - }, -}); - -var TabStateInternal = { - /** - * Processes a data update sent by the content script. - */ - update: function (browser, {data}) { - TabStateCache.update(browser, data); - }, - - /** - * Collect data related to a single tab, synchronously. - * - * @param tab - * tabbrowser tab - * - * @returns {TabData} An object with the data for this tab. If the - * tab has not been invalidated since the last call to - * collect(aTab), the same object is returned. - */ - collect: function (tab) { - return this._collectBaseTabData(tab); - }, - - /** - * Collect data related to a single tab, including private data. - * Use with caution. - * - * @param tab - * tabbrowser tab - * - * @returns {object} An object with the data for this tab. This data is never - * cached, it will always be read from the tab and thus be - * up-to-date. - */ - clone: function (tab) { - return this._collectBaseTabData(tab, {includePrivateData: true}); - }, - - /** - * Collects basic tab data for a given tab. - * - * @param tab - * tabbrowser tab - * @param options (object) - * {includePrivateData: true} to always include private data - * - * @returns {object} An object with the basic data for this tab. - */ - _collectBaseTabData: function (tab, options) { - let tabData = { entries: [], lastAccessed: tab.lastAccessed }; - let browser = tab.linkedBrowser; - - if (tab.pinned) { - tabData.pinned = true; - } - - tabData.hidden = tab.hidden; - - if (browser.audioMuted) { - tabData.muted = true; - tabData.muteReason = tab.muteReason; - } - - // Save tab attributes. - tabData.attributes = TabAttributes.get(tab); - - if (tab.__SS_extdata) { - tabData.extData = tab.__SS_extdata; - } - - // Copy data from the tab state cache only if the tab has fully finished - // restoring. We don't want to overwrite data contained in __SS_data. - this.copyFromCache(browser, tabData, options); - - // After copyFromCache() was called we check for properties that are kept - // in the cache only while the tab is pending or restoring. Once that - // happened those properties will be removed from the cache and will - // be read from the tab/browser every time we collect data. - - // Store the tab icon. - if (!("image" in tabData)) { - let tabbrowser = tab.ownerGlobal.gBrowser; - tabData.image = tabbrowser.getIcon(tab); - } - - // Store the serialized contentPrincipal of this tab to use for the icon. - if (!("iconLoadingPrincipal" in tabData)) { - tabData.iconLoadingPrincipal = Utils.serializePrincipal(browser.contentPrincipal); - } - - // If there is a userTypedValue set, then either the user has typed something - // in the URL bar, or a new tab was opened with a URI to load. - // If so, we also track whether we were still in the process of loading something. - if (!("userTypedValue" in tabData) && browser.userTypedValue) { - tabData.userTypedValue = browser.userTypedValue; - // We always used to keep track of the loading state as an integer, where - // '0' indicated the user had typed since the last load (or no load was - // ongoing), and any positive value indicated we had started a load since - // the last time the user typed in the URL bar. Mimic this to keep the - // session store representation in sync, even though we now represent this - // more explicitly: - tabData.userTypedClear = browser.didStartLoadSinceLastUserTyping() ? 1 : 0; - } - - return tabData; - }, - - /** - * Copy data for the given |browser| from the cache to |tabData|. - * - * @param browser (xul:browser) - * The browser belonging to the given |tabData| object. - * @param tabData (object) - * The tab data belonging to the given |tab|. - * @param options (object) - * {includePrivateData: true} to always include private data - */ - copyFromCache(browser, tabData, options = {}) { - let data = TabStateCache.get(browser); - if (!data) { - return; - } - - // The caller may explicitly request to omit privacy checks. - let includePrivateData = options && options.includePrivateData; - let isPinned = !!tabData.pinned; - - for (let key of Object.keys(data)) { - let value = data[key]; - - // Filter sensitive data according to the current privacy level. - if (!includePrivateData) { - if (key === "storage") { - value = PrivacyFilter.filterSessionStorageData(value); - } else if (key === "formdata") { - value = PrivacyFilter.filterFormData(value); - } - } - - if (key === "history") { - tabData.entries = value.entries; - - if (value.hasOwnProperty("userContextId")) { - tabData.userContextId = value.userContextId; - } - - if (value.hasOwnProperty("index")) { - tabData.index = value.index; - } - } else { - tabData[key] = value; - } - } - } -}; diff --git a/browser/components/sessionstore/TabStateCache.jsm b/browser/components/sessionstore/TabStateCache.jsm deleted file mode 100644 index 9bed315a0..000000000 --- a/browser/components/sessionstore/TabStateCache.jsm +++ /dev/null @@ -1,163 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["TabStateCache"]; - -/** - * A cache for tabs data. - * - * This cache implements a weak map from tabs (as XUL elements) - * to tab data (as objects). - * - * Note that we should never cache private data, as: - * - that data is used very seldom by SessionStore; - * - caching private data in addition to public data is memory consuming. - */ -this.TabStateCache = Object.freeze({ - /** - * Retrieves cached data for a given |tab| or associated |browser|. - * - * @param browserOrTab (xul:tab or xul:browser) - * The tab or browser to retrieve cached data for. - * @return (object) - * The cached data stored for the given |tab| - * or associated |browser|. - */ - get: function (browserOrTab) { - return TabStateCacheInternal.get(browserOrTab); - }, - - /** - * Updates cached data for a given |tab| or associated |browser|. - * - * @param browserOrTab (xul:tab or xul:browser) - * The tab or browser belonging to the given tab data. - * @param newData (object) - * The new data to be stored for the given |tab| - * or associated |browser|. - */ - update: function (browserOrTab, newData) { - TabStateCacheInternal.update(browserOrTab, newData); - } -}); - -var TabStateCacheInternal = { - _data: new WeakMap(), - - /** - * Retrieves cached data for a given |tab| or associated |browser|. - * - * @param browserOrTab (xul:tab or xul:browser) - * The tab or browser to retrieve cached data for. - * @return (object) - * The cached data stored for the given |tab| - * or associated |browser|. - */ - get: function (browserOrTab) { - return this._data.get(browserOrTab.permanentKey); - }, - - /** - * Helper function used by update (see below). For message size - * optimization sometimes we don't update the whole session storage - * only the values that have been changed. - * - * @param data (object) - * The cached data where we want to update the changes. - * @param change (object) - * The actual changed values per domain. - */ - updatePartialStorageChange: function (data, change) { - if (!data.storage) { - data.storage = {}; - } - - let storage = data.storage; - for (let domain of Object.keys(change)) { - for (let key of Object.keys(change[domain])) { - let value = change[domain][key]; - if (value === null) { - if (storage[domain] && storage[domain][key]) { - delete storage[domain][key]; - } - } else { - if (!storage[domain]) { - storage[domain] = {}; - } - storage[domain][key] = value; - } - } - } - }, - - /** - * Helper function used by update (see below). For message size - * optimization sometimes we don't update the whole browser history - * only the current index and the tail of the history from a certain - * index (specified by change.fromIdx) - * - * @param data (object) - * The cached data where we want to update the changes. - * @param change (object) - * Object containing the tail of the history array, and - * some additional metadata. - */ - updatePartialHistoryChange: function (data, change) { - const kLastIndex = Number.MAX_SAFE_INTEGER - 1; - - if (!data.history) { - data.history = { entries: [] }; - } - - let history = data.history; - for (let key of Object.keys(change)) { - if (key == "entries") { - if (change.fromIdx != kLastIndex) { - history.entries.splice(change.fromIdx + 1); - while (change.entries.length) { - history.entries.push(change.entries.shift()); - } - } - } else if (key != "fromIndex") { - history[key] = change[key]; - } - } - }, - - /** - * Updates cached data for a given |tab| or associated |browser|. - * - * @param browserOrTab (xul:tab or xul:browser) - * The tab or browser belonging to the given tab data. - * @param newData (object) - * The new data to be stored for the given |tab| - * or associated |browser|. - */ - update: function (browserOrTab, newData) { - let data = this._data.get(browserOrTab.permanentKey) || {}; - - for (let key of Object.keys(newData)) { - if (key == "storagechange") { - this.updatePartialStorageChange(data, newData.storagechange); - continue; - } - - if (key == "historychange") { - this.updatePartialHistoryChange(data, newData.historychange); - continue; - } - - let value = newData[key]; - if (value === null) { - delete data[key]; - } else { - data[key] = value; - } - } - - this._data.set(browserOrTab.permanentKey, data); - } -}; diff --git a/browser/components/sessionstore/TabStateFlusher.jsm b/browser/components/sessionstore/TabStateFlusher.jsm deleted file mode 100644 index 6397efe9d..000000000 --- a/browser/components/sessionstore/TabStateFlusher.jsm +++ /dev/null @@ -1,184 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["TabStateFlusher"]; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/Promise.jsm", this); - -/** - * A module that enables async flushes. Updates from frame scripts are - * throttled to be sent only once per second. If an action wants a tab's latest - * state without waiting for a second then it can request an async flush and - * wait until the frame scripts reported back. At this point the parent has the - * latest data and the action can continue. - */ -this.TabStateFlusher = Object.freeze({ - /** - * Requests an async flush for the given browser. Returns a promise that will - * resolve when we heard back from the content process and the parent has - * all the latest data. - */ - flush(browser) { - return TabStateFlusherInternal.flush(browser); - }, - - /** - * Requests an async flush for all browsers of a given window. Returns a Promise - * that will resolve when we've heard back from all browsers. - */ - flushWindow(window) { - return TabStateFlusherInternal.flushWindow(window); - }, - - /** - * Resolves the flush request with the given flush ID. - * - * @param browser (<xul:browser>) - * The browser for which the flush is being resolved. - * @param flushID (int) - * The ID of the flush that was sent to the browser. - * @param success (bool, optional) - * Whether or not the flush succeeded. - * @param message (string, optional) - * An error message that will be sent to the Console in the - * event that a flush failed. - */ - resolve(browser, flushID, success=true, message="") { - TabStateFlusherInternal.resolve(browser, flushID, success, message); - }, - - /** - * Resolves all active flush requests for a given browser. This should be - * used when the content process crashed or the final update message was - * seen. In those cases we can't guarantee to ever hear back from the frame - * script so we just resolve all requests instead of discarding them. - * - * @param browser (<xul:browser>) - * The browser for which all flushes are being resolved. - * @param success (bool, optional) - * Whether or not the flushes succeeded. - * @param message (string, optional) - * An error message that will be sent to the Console in the - * event that the flushes failed. - */ - resolveAll(browser, success=true, message="") { - TabStateFlusherInternal.resolveAll(browser, success, message); - } -}); - -var TabStateFlusherInternal = { - // Stores the last request ID. - _lastRequestID: 0, - - // A map storing all active requests per browser. - _requests: new WeakMap(), - - /** - * Requests an async flush for the given browser. Returns a promise that will - * resolve when we heard back from the content process and the parent has - * all the latest data. - */ - flush(browser) { - let id = ++this._lastRequestID; - let mm = browser.messageManager; - mm.sendAsyncMessage("SessionStore:flush", {id}); - - // Retrieve active requests for given browser. - let permanentKey = browser.permanentKey; - let perBrowserRequests = this._requests.get(permanentKey) || new Map(); - - return new Promise(resolve => { - // Store resolve() so that we can resolve the promise later. - perBrowserRequests.set(id, resolve); - - // Update the flush requests stored per browser. - this._requests.set(permanentKey, perBrowserRequests); - }); - }, - - /** - * Requests an async flush for all browsers of a given window. Returns a Promise - * that will resolve when we've heard back from all browsers. - */ - flushWindow(window) { - let browsers = window.gBrowser.browsers; - let promises = browsers.map((browser) => this.flush(browser)); - return Promise.all(promises); - }, - - /** - * Resolves the flush request with the given flush ID. - * - * @param browser (<xul:browser>) - * The browser for which the flush is being resolved. - * @param flushID (int) - * The ID of the flush that was sent to the browser. - * @param success (bool, optional) - * Whether or not the flush succeeded. - * @param message (string, optional) - * An error message that will be sent to the Console in the - * event that a flush failed. - */ - resolve(browser, flushID, success=true, message="") { - // Nothing to do if there are no pending flushes for the given browser. - if (!this._requests.has(browser.permanentKey)) { - return; - } - - // Retrieve active requests for given browser. - let perBrowserRequests = this._requests.get(browser.permanentKey); - if (!perBrowserRequests.has(flushID)) { - return; - } - - if (!success) { - Cu.reportError("Failed to flush browser: " + message); - } - - // Resolve the request with the given id. - let resolve = perBrowserRequests.get(flushID); - perBrowserRequests.delete(flushID); - resolve(success); - }, - - /** - * Resolves all active flush requests for a given browser. This should be - * used when the content process crashed or the final update message was - * seen. In those cases we can't guarantee to ever hear back from the frame - * script so we just resolve all requests instead of discarding them. - * - * @param browser (<xul:browser>) - * The browser for which all flushes are being resolved. - * @param success (bool, optional) - * Whether or not the flushes succeeded. - * @param message (string, optional) - * An error message that will be sent to the Console in the - * event that the flushes failed. - */ - resolveAll(browser, success=true, message="") { - // Nothing to do if there are no pending flushes for the given browser. - if (!this._requests.has(browser.permanentKey)) { - return; - } - - // Retrieve active requests for given browser. - let perBrowserRequests = this._requests.get(browser.permanentKey); - - if (!success) { - Cu.reportError("Failed to flush browser: " + message); - } - - // Resolve all requests. - for (let resolve of perBrowserRequests.values()) { - resolve(success); - } - - // Clear active requests. - perBrowserRequests.clear(); - } -}; diff --git a/browser/components/sessionstore/content/aboutSessionRestore.js b/browser/components/sessionstore/content/aboutSessionRestore.js deleted file mode 100644 index 8f265235d..000000000 --- a/browser/components/sessionstore/content/aboutSessionRestore.js +++ /dev/null @@ -1,373 +0,0 @@ -/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); - -var gStateObject; -var gTreeData; - -// Page initialization - -window.onload = function() { - // pages used by this script may have a link that needs to be updated to - // the in-product link. - let anchor = document.getElementById("linkMoreTroubleshooting"); - if (anchor) { - let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); - anchor.setAttribute("href", baseURL + "troubleshooting"); - } - - // wire up click handlers for the radio buttons if they exist. - for (let radioId of ["radioRestoreAll", "radioRestoreChoose"]) { - let button = document.getElementById(radioId); - if (button) { - button.addEventListener("click", updateTabListVisibility); - } - } - - // the crashed session state is kept inside a textbox so that SessionStore picks it up - // (for when the tab is closed or the session crashes right again) - var sessionData = document.getElementById("sessionData"); - if (!sessionData.value) { - document.getElementById("errorTryAgain").disabled = true; - return; - } - - try { - gStateObject = JSON.parse(sessionData.value); - } catch (e) { - Cu.reportError(e); - } - - // make sure the data is tracked to be restored in case of a subsequent crash - var event = document.createEvent("UIEvents"); - event.initUIEvent("input", true, true, window, 0); - sessionData.dispatchEvent(event); - - initTreeView(); - - document.getElementById("errorTryAgain").focus(); -}; - -function isTreeViewVisible() { - let tabList = document.querySelector(".tree-container"); - return tabList.hasAttribute("available"); -} - -function initTreeView() { - // If we aren't visible we initialize as we are made visible (and it's OK - // to initialize multiple times) - if (!isTreeViewVisible()) { - return; - } - var tabList = document.getElementById("tabList"); - var winLabel = tabList.getAttribute("_window_label"); - - gTreeData = []; - if (gStateObject) { - gStateObject.windows.forEach(function(aWinData, aIx) { - var winState = { - label: winLabel.replace("%S", (aIx + 1)), - open: true, - checked: true, - ix: aIx - }; - winState.tabs = aWinData.tabs.map(function(aTabData) { - var entry = aTabData.entries[aTabData.index - 1] || { url: "about:blank" }; - var iconURL = aTabData.image || null; - // don't initiate a connection just to fetch a favicon (see bug 462863) - if (/^https?:/.test(iconURL)) - iconURL = "moz-anno:favicon:" + iconURL; - return { - label: entry.title || entry.url, - checked: true, - src: iconURL, - parent: winState - }; - }); - gTreeData.push(winState); - for (let tab of winState.tabs) - gTreeData.push(tab); - }, this); - } - - tabList.view = treeView; - tabList.view.selection.select(0); -} - -// User actions -function updateTabListVisibility() { - let tabList = document.querySelector(".tree-container"); - let container = document.querySelector(".container"); - if (document.getElementById("radioRestoreChoose").checked) { - tabList.setAttribute("available", "true"); - container.classList.add("restore-chosen"); - } else { - tabList.removeAttribute("available"); - container.classList.remove("restore-chosen"); - } - initTreeView(); -} - -function restoreSession() { - Services.obs.notifyObservers(null, "sessionstore-initiating-manual-restore", ""); - document.getElementById("errorTryAgain").disabled = true; - - if (isTreeViewVisible()) { - if (!gTreeData.some(aItem => aItem.checked)) { - // This should only be possible when we have no "cancel" button, and thus - // the "Restore session" button always remains enabled. In that case and - // when nothing is selected, we just want a new session. - startNewSession(); - return; - } - - // remove all unselected tabs from the state before restoring it - var ix = gStateObject.windows.length - 1; - for (var t = gTreeData.length - 1; t >= 0; t--) { - if (treeView.isContainer(t)) { - if (gTreeData[t].checked === 0) - // this window will be restored partially - gStateObject.windows[ix].tabs = - gStateObject.windows[ix].tabs.filter((aTabData, aIx) => - gTreeData[t].tabs[aIx].checked); - else if (!gTreeData[t].checked) - // this window won't be restored at all - gStateObject.windows.splice(ix, 1); - ix--; - } - } - } - var stateString = JSON.stringify(gStateObject); - - var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); - var top = getBrowserWindow(); - - // if there's only this page open, reuse the window for restoring the session - if (top.gBrowser.tabs.length == 1) { - ss.setWindowState(top, stateString, true); - return; - } - - // restore the session into a new window and close the current tab - var newWindow = top.openDialog(top.location, "_blank", "chrome,dialog=no,all"); - - var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); - obs.addObserver(function observe(win, topic) { - if (win != newWindow) { - return; - } - - obs.removeObserver(observe, topic); - ss.setWindowState(newWindow, stateString, true); - - var tabbrowser = top.gBrowser; - var tabIndex = tabbrowser.getBrowserIndexForDocument(document); - tabbrowser.removeTab(tabbrowser.tabs[tabIndex]); - }, "browser-delayed-startup-finished", false); -} - -function startNewSession() { - var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); - if (prefBranch.getIntPref("browser.startup.page") == 0) - getBrowserWindow().gBrowser.loadURI("about:blank"); - else - getBrowserWindow().BrowserHome(); -} - -function onListClick(aEvent) { - // don't react to right-clicks - if (aEvent.button == 2) - return; - - if (!treeView.treeBox) { - return; - } - var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY); - if (cell.col) { - // Restore this specific tab in the same window for middle/double/accel clicking - // on a tab's title. - let accelKey = AppConstants.platform == "macosx" ? - aEvent.metaKey : - aEvent.ctrlKey; - if ((aEvent.button == 1 || aEvent.button == 0 && aEvent.detail == 2 || accelKey) && - cell.col.id == "title" && - !treeView.isContainer(cell.row)) { - restoreSingleTab(cell.row, aEvent.shiftKey); - aEvent.stopPropagation(); - } - else if (cell.col.id == "restore") - toggleRowChecked(cell.row); - } -} - -function onListKeyDown(aEvent) { - switch (aEvent.keyCode) - { - case KeyEvent.DOM_VK_SPACE: - toggleRowChecked(document.getElementById("tabList").currentIndex); - // Prevent page from scrolling on the space key. - aEvent.preventDefault(); - break; - case KeyEvent.DOM_VK_RETURN: - var ix = document.getElementById("tabList").currentIndex; - if (aEvent.ctrlKey && !treeView.isContainer(ix)) - restoreSingleTab(ix, aEvent.shiftKey); - break; - } -} - -// Helper functions - -function getBrowserWindow() { - return window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem - .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); -} - -function toggleRowChecked(aIx) { - function isChecked(aItem) { - return aItem.checked; - } - - var item = gTreeData[aIx]; - item.checked = !item.checked; - treeView.treeBox.invalidateRow(aIx); - - if (treeView.isContainer(aIx)) { - // (un)check all tabs of this window as well - for (let tab of item.tabs) { - tab.checked = item.checked; - treeView.treeBox.invalidateRow(gTreeData.indexOf(tab)); - } - } - else { - // update the window's checkmark as well (0 means "partially checked") - item.parent.checked = item.parent.tabs.every(isChecked) ? true : - item.parent.tabs.some(isChecked) ? 0 : false; - treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent)); - } - - // we only disable the button when there's no cancel button. - if (document.getElementById("errorCancel")) { - document.getElementById("errorTryAgain").disabled = !gTreeData.some(isChecked); - } -} - -function restoreSingleTab(aIx, aShifted) { - var tabbrowser = getBrowserWindow().gBrowser; - var newTab = tabbrowser.addTab(); - var item = gTreeData[aIx]; - - var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); - var tabState = gStateObject.windows[item.parent.ix] - .tabs[aIx - gTreeData.indexOf(item.parent) - 1]; - // ensure tab would be visible on the tabstrip. - tabState.hidden = false; - ss.setTabState(newTab, JSON.stringify(tabState)); - - // respect the preference as to whether to select the tab (the Shift key inverses) - var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); - if (prefBranch.getBoolPref("browser.tabs.loadInBackground") != !aShifted) - tabbrowser.selectedTab = newTab; -} - -// Tree controller - -var treeView = { - treeBox: null, - selection: null, - - get rowCount() { return gTreeData.length; }, - setTree: function(treeBox) { this.treeBox = treeBox; }, - getCellText: function(idx, column) { return gTreeData[idx].label; }, - isContainer: function(idx) { - return gTreeData[idx] ? "open" in gTreeData[idx] : false; - }, - getCellValue: function(idx, column){ return gTreeData[idx].checked; }, - isContainerOpen: function(idx) { return gTreeData[idx].open; }, - isContainerEmpty: function(idx) { return false; }, - isSeparator: function(idx) { return false; }, - isSorted: function() { return false; }, - isEditable: function(idx, column) { return false; }, - canDrop: function(idx, orientation, dt) { return false; }, - getLevel: function(idx) { return this.isContainer(idx) ? 0 : 1; }, - - getParentIndex: function(idx) { - if (!this.isContainer(idx)) - for (var t = idx - 1; t >= 0 ; t--) - if (this.isContainer(t)) - return t; - return -1; - }, - - hasNextSibling: function(idx, after) { - var thisLevel = this.getLevel(idx); - for (var t = after + 1; t < gTreeData.length; t++) - if (this.getLevel(t) <= thisLevel) - return this.getLevel(t) == thisLevel; - return false; - }, - - toggleOpenState: function(idx) { - if (!this.isContainer(idx)) - return; - var item = gTreeData[idx]; - if (item.open) { - // remove this window's tab rows from the view - var thisLevel = this.getLevel(idx); - for (var t = idx + 1; t < gTreeData.length && this.getLevel(t) > thisLevel; t++); - var deletecount = t - idx - 1; - gTreeData.splice(idx + 1, deletecount); - this.treeBox.rowCountChanged(idx + 1, -deletecount); - } - else { - // add this window's tab rows to the view - var toinsert = gTreeData[idx].tabs; - for (var i = 0; i < toinsert.length; i++) - gTreeData.splice(idx + i + 1, 0, toinsert[i]); - this.treeBox.rowCountChanged(idx + 1, toinsert.length); - } - item.open = !item.open; - this.treeBox.invalidateRow(idx); - }, - - getCellProperties: function(idx, column) { - if (column.id == "restore" && this.isContainer(idx) && gTreeData[idx].checked === 0) - return "partial"; - if (column.id == "title") - return this.getImageSrc(idx, column) ? "icon" : "noicon"; - - return ""; - }, - - getRowProperties: function(idx) { - var winState = gTreeData[idx].parent || gTreeData[idx]; - if (winState.ix % 2 != 0) - return "alternate"; - - return ""; - }, - - getImageSrc: function(idx, column) { - if (column.id == "title") - return gTreeData[idx].src || null; - return null; - }, - - getProgressMode : function(idx, column) { }, - cycleHeader: function(column) { }, - cycleCell: function(idx, column) { }, - selectionChanged: function() { }, - performAction: function(action) { }, - performActionOnCell: function(action, index, column) { }, - getColumnProperties: function(column) { return ""; } -}; diff --git a/browser/components/sessionstore/content/aboutSessionRestore.xhtml b/browser/components/sessionstore/content/aboutSessionRestore.xhtml deleted file mode 100644 index bcd9084e7..000000000 --- a/browser/components/sessionstore/content/aboutSessionRestore.xhtml +++ /dev/null @@ -1,86 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. ---> -<!DOCTYPE html [ - <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> - %htmlDTD; - <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd"> - %netErrorDTD; - <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> - %globalDTD; - <!ENTITY % restorepageDTD SYSTEM "chrome://browser/locale/aboutSessionRestore.dtd"> - %restorepageDTD; -]> - -<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - <head> - <title>&restorepage.tabtitle;</title> - <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css" media="all"/> - <link rel="stylesheet" href="chrome://browser/skin/aboutSessionRestore.css" type="text/css" media="all"/> - <link rel="icon" type="image/png" href="chrome://global/skin/icons/warning-16.png"/> - - <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutSessionRestore.js"/> - </head> - - <body dir="&locale.dir;"> - - <div class="container restore-chosen"> - - <div class="title"> - <h1 class="title-text">&restorepage.errorTitle;</h1> - </div> - <div class="description"> - <p>&restorepage.problemDesc;</p> - - <div id="errorLongDesc"> - <p>&restorepage.tryThis;</p> - <ul> - <li>&restorepage.restoreSome;</li> - <li>&restorepage.startNew;</li> - </ul> - </div> - </div> - <div class="tree-container" available="true"> - <xul:tree id="tabList" seltype="single" hidecolumnpicker="true" - onclick="onListClick(event);" onkeydown="onListKeyDown(event);" - _window_label="&restorepage.windowLabel;"> - <xul:treecols> - <xul:treecol cycler="true" id="restore" type="checkbox" label="&restorepage.restoreHeader;"/> - <xul:splitter class="tree-splitter"/> - <xul:treecol primary="true" id="title" label="&restorepage.listHeader;" flex="1"/> - </xul:treecols> - <xul:treechildren flex="1"/> - </xul:tree> - </div> - <div class="button-container"> -#ifdef XP_UNIX - <xul:button id="errorCancel" - label="&restorepage.closeButton;" - accesskey="&restorepage.close.access;" - oncommand="startNewSession();"/> - <xul:button class="primary" - id="errorTryAgain" - label="&restorepage.tryagainButton;" - accesskey="&restorepage.restore.access;" - oncommand="restoreSession();"/> -#else - <xul:button class="primary" - id="errorTryAgain" - label="&restorepage.tryagainButton;" - accesskey="&restorepage.restore.access;" - oncommand="restoreSession();"/> - <xul:button id="errorCancel" - label="&restorepage.closeButton;" - accesskey="&restorepage.close.access;" - oncommand="startNewSession();"/> -#endif - </div> - <!-- holds the session data for when the tab is closed --> - <input type="text" id="sessionData" style="display: none;"/> - </div> - - </body> -</html> diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js deleted file mode 100644 index 858e35750..000000000 --- a/browser/components/sessionstore/content/content-sessionStore.js +++ /dev/null @@ -1,897 +0,0 @@ -/* 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"; - -function debug(msg) { - Services.console.logStringMessage("SessionStoreContent: " + msg); -} - -var Cu = Components.utils; -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); -Cu.import("resource://gre/modules/Timer.jsm", this); - -XPCOMUtils.defineLazyModuleGetter(this, "FormData", - "resource://gre/modules/FormData.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Preferences", - "resource://gre/modules/Preferences.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities", - "resource:///modules/sessionstore/DocShellCapabilities.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PageStyle", - "resource:///modules/sessionstore/PageStyle.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", - "resource://gre/modules/ScrollPosition.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", - "resource:///modules/sessionstore/SessionHistory.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", - "resource:///modules/sessionstore/SessionStorage.jsm"); - -Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this); -var gFrameTree = new FrameTree(this); - -Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this); -XPCOMUtils.defineLazyGetter(this, 'gContentRestore', - () => { return new ContentRestore(this) }); - -// The current epoch. -var gCurrentEpoch = 0; - -// A bound to the size of data to store for DOM Storage. -const DOM_STORAGE_MAX_CHARS = 10000000; // 10M characters - -// This pref controls whether or not we send updates to the parent on a timeout -// or not, and should only be used for tests or debugging. -const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates"; - -const kNoIndex = Number.MAX_SAFE_INTEGER; -const kLastIndex = Number.MAX_SAFE_INTEGER - 1; - -/** - * Returns a lazy function that will evaluate the given - * function |fn| only once and cache its return value. - */ -function createLazy(fn) { - let cached = false; - let cachedValue = null; - - return function lazy() { - if (!cached) { - cachedValue = fn(); - cached = true; - } - - return cachedValue; - }; -} - -/** - * Listens for and handles content events that we need for the - * session store service to be notified of state changes in content. - */ -var EventListener = { - - init: function () { - addEventListener("load", this, true); - }, - - handleEvent: function (event) { - // Ignore load events from subframes. - if (event.target != content.document) { - return; - } - - if (content.document.documentURI.startsWith("about:reader")) { - if (event.type == "load" && - !content.document.body.classList.contains("loaded")) { - // Don't restore the scroll position of an about:reader page at this - // point; listen for the custom event dispatched from AboutReader.jsm. - content.addEventListener("AboutReaderContentReady", this); - return; - } - - content.removeEventListener("AboutReaderContentReady", this); - } - - // Restore the form data and scroll position. If we're not currently - // restoring a tab state then this call will simply be a noop. - gContentRestore.restoreDocument(); - } -}; - -/** - * Listens for and handles messages sent by the session store service. - */ -var MessageListener = { - - MESSAGES: [ - "SessionStore:restoreHistory", - "SessionStore:restoreTabContent", - "SessionStore:resetRestore", - "SessionStore:flush", - ], - - init: function () { - this.MESSAGES.forEach(m => addMessageListener(m, this)); - }, - - receiveMessage: function ({name, data}) { - // The docShell might be gone. Don't process messages, - // that will just lead to errors anyway. - if (!docShell) { - return; - } - - // A fresh tab always starts with epoch=0. The parent has the ability to - // override that to signal a new era in this tab's life. This enables it - // to ignore async messages that were already sent but not yet received - // and would otherwise confuse the internal tab state. - if (data.epoch && data.epoch != gCurrentEpoch) { - gCurrentEpoch = data.epoch; - } - - switch (name) { - case "SessionStore:restoreHistory": - this.restoreHistory(data); - break; - case "SessionStore:restoreTabContent": - this.restoreTabContent(data); - break; - case "SessionStore:resetRestore": - gContentRestore.resetRestore(); - break; - case "SessionStore:flush": - this.flush(data); - break; - default: - debug("received unknown message '" + name + "'"); - break; - } - }, - - restoreHistory({epoch, tabData, loadArguments, isRemotenessUpdate}) { - gContentRestore.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. - sendSyncMessage("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. - sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch}); - } - }); - - // When restoreHistory finishes, we send a synchronous message to - // SessionStore.jsm so that it can run SSTabRestoring. Users of - // SSTabRestoring seem to get confused if chrome and content are out of - // sync about the state of the restore (particularly regarding - // docShell.currentURI). Using a synchronous message is the easiest way - // to temporarily synchronize them. - sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate}); - }, - - restoreTabContent({loadArguments, isRemotenessUpdate}) { - let epoch = gCurrentEpoch; - - // We need to pass the value of didStartLoad back to SessionStore.jsm. - let didStartLoad = gContentRestore.restoreTabContent(loadArguments, isRemotenessUpdate, () => { - // Tell SessionStore.jsm that it may want to restore some more tabs, - // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. - sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate}); - }); - - sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch, isRemotenessUpdate}); - - if (!didStartLoad) { - // Pretend that the load succeeded so that event handlers fire correctly. - sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate}); - } - }, - - flush({id}) { - // Flush the message queue, send the latest updates. - MessageQueue.send({flushID: id}); - } -}; - -/** - * 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} - */ -var SessionHistoryListener = { - init: function () { - // The frame tree observer is needed to handle initial subframe loads. - // It will redundantly invalidate with the SHistoryListener in some cases - // but these invalidations are very cheap. - gFrameTree.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. - docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory. - addSHistoryListener(this); - - // Collect data if we start with a non-empty shistory. - if (!SessionHistory.isEmpty(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. - MessageQueue.send(); - } - - // Listen for page title changes. - addEventListener("DOMTitleChanged", this); - }, - - uninit: function () { - let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; - if (sessionHistory) { - sessionHistory.removeSHistoryListener(this); - } - }, - - collect: function () { - this._fromIdx = kNoIndex; - if (docShell) { - MessageQueue.push("history", () => SessionHistory.collect(docShell)); - } - }, - - _fromIdx: kNoIndex, - - // 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: function (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; - MessageQueue.push("historychange", () => { - if (this._fromIdx === kNoIndex) { - return null; - } - - let history = SessionHistory.collect(docShell); - if (kLastIndex == idx) { - history.entries = []; - } else { - history.entries.splice(0, this._fromIdx + 1); - } - - history.fromIdx = this._fromIdx; - - this._fromIdx = kNoIndex; - return history; - }); - }, - - handleEvent(event) { - this.collect(); - }, - - onFrameTreeCollected: function () { - this.collect(); - }, - - onFrameTreeReset: function () { - this.collect(); - }, - - OnHistoryNewEntry: function (newURI, oldIndex) { - this.collectFrom(oldIndex); - }, - - OnHistoryGoBack: function (backURI) { - this.collectFrom(kLastIndex); - return true; - }, - - OnHistoryGoForward: function (forwardURI) { - this.collectFrom(kLastIndex); - return true; - }, - - OnHistoryGotoIndex: function (index, gotoURI) { - this.collectFrom(kLastIndex); - return true; - }, - - OnHistoryPurge: function (numEntries) { - this.collect(); - return true; - }, - - OnHistoryReload: function (reloadURI, reloadFlags) { - this.collect(); - return true; - }, - - OnHistoryReplaceEntry: function (index) { - this.collect(); - }, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsISHistoryListener, - Ci.nsISupportsWeakReference - ]) -}; - -/** - * Listens for scroll position changes. Whenever the user scrolls the top-most - * frame we update the scroll position and will restore it when requested. - * - * Causes a SessionStore:update message to be sent that contains the current - * scroll positions as a tree of strings. If no frame of the whole frame tree - * is scrolled this will return null so that we don't tack a property onto - * the tabData object in the parent process. - * - * Example: - * {scroll: "100,100", children: [null, null, {scroll: "200,200"}]} - */ -var ScrollPositionListener = { - init: function () { - addEventListener("scroll", this); - gFrameTree.addObserver(this); - }, - - handleEvent: function (event) { - let frame = event.target.defaultView; - - // Don't collect scroll data for frames created at or after the load event - // as SessionStore can't restore scroll data for those. - if (gFrameTree.contains(frame)) { - MessageQueue.push("scroll", () => this.collect()); - } - }, - - onFrameTreeCollected: function () { - MessageQueue.push("scroll", () => this.collect()); - }, - - onFrameTreeReset: function () { - MessageQueue.push("scroll", () => null); - }, - - collect: function () { - return gFrameTree.map(ScrollPosition.collect); - } -}; - -/** - * Listens for changes to input elements. Whenever the value of an input - * element changes we will re-collect data for the current frame tree and send - * a message to the parent process. - * - * Causes a SessionStore:update message to be sent that contains the form data - * for all reachable frames. - * - * Example: - * { - * formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}}, - * children: [ - * null, - * {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}} - * ] - * } - */ -var FormDataListener = { - init: function () { - addEventListener("input", this, true); - addEventListener("change", this, true); - gFrameTree.addObserver(this); - }, - - handleEvent: function (event) { - let frame = event.target.ownerGlobal; - - // Don't collect form data for frames created at or after the load event - // as SessionStore can't restore form data for those. - if (gFrameTree.contains(frame)) { - MessageQueue.push("formdata", () => this.collect()); - } - }, - - onFrameTreeReset: function () { - MessageQueue.push("formdata", () => null); - }, - - collect: function () { - return gFrameTree.map(FormData.collect); - } -}; - -/** - * Listens for changes to the page style. Whenever a different page style is - * selected or author styles are enabled/disabled we send a message with the - * currently applied style to the chrome process. - * - * Causes a SessionStore:update message to be sent that contains the currently - * selected pageStyle for all reachable frames. - * - * Example: - * {pageStyle: "Dusk", children: [null, {pageStyle: "Mozilla"}]} - */ -var PageStyleListener = { - init: function () { - Services.obs.addObserver(this, "author-style-disabled-changed", false); - Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false); - gFrameTree.addObserver(this); - }, - - uninit: function () { - Services.obs.removeObserver(this, "author-style-disabled-changed"); - Services.obs.removeObserver(this, "style-sheet-applicable-state-changed"); - }, - - observe: function (subject, topic) { - let frame = subject.defaultView; - - if (frame && gFrameTree.contains(frame)) { - MessageQueue.push("pageStyle", () => this.collect()); - } - }, - - collect: function () { - return PageStyle.collect(docShell, gFrameTree); - }, - - onFrameTreeCollected: function () { - MessageQueue.push("pageStyle", () => this.collect()); - }, - - onFrameTreeReset: function () { - MessageQueue.push("pageStyle", () => null); - } -}; - -/** - * Listens for changes to docShell capabilities. Whenever a new load is started - * we need to re-check the list of capabilities and send message when it has - * changed. - * - * Causes a SessionStore:update message to be sent that contains the currently - * disabled docShell capabilities (all nsIDocShell.allow* properties set to - * false) as a string - i.e. capability names separate by commas. - */ -var DocShellCapabilitiesListener = { - /** - * This field is used to compare the last docShell capabilities to the ones - * that have just been collected. If nothing changed we won't send a message. - */ - _latestCapabilities: "", - - init: function () { - gFrameTree.addObserver(this); - }, - - /** - * onFrameTreeReset() is called as soon as we start loading a page. - */ - onFrameTreeReset: function() { - // The order of docShell capabilities cannot change while we're running - // so calling join() without sorting before is totally sufficient. - let caps = DocShellCapabilities.collect(docShell).join(","); - - // Send new data only when the capability list changes. - if (caps != this._latestCapabilities) { - this._latestCapabilities = caps; - MessageQueue.push("disallow", () => caps || null); - } - } -}; - -/** - * Listens for changes to the DOMSessionStorage. Whenever new keys are added, - * existing ones removed or changed, or the storage is cleared we will send a - * message to the parent process containing up-to-date sessionStorage data. - * - * Causes a SessionStore:update message to be sent that contains the current - * DOMSessionStorage contents. The data is a nested object using host names - * as keys and per-host DOMSessionStorage data as values. - */ -var SessionStorageListener = { - init: function () { - addEventListener("MozSessionStorageChanged", this, true); - Services.obs.addObserver(this, "browser:purge-domain-data", false); - gFrameTree.addObserver(this); - }, - - uninit: function () { - Services.obs.removeObserver(this, "browser:purge-domain-data"); - }, - - handleEvent: function (event) { - if (gFrameTree.contains(event.target)) { - this.collectFromEvent(event); - } - }, - - observe: function () { - // Collect data on the next tick so that any other observer - // that needs to purge data can do its work first. - setTimeout(() => this.collect(), 0); - }, - - // Before DOM Storage can be written to disk, it needs to be serialized - // for sending across frames/processes, then again to be sent across - // threads, then again to be put in a buffer for the disk. Each of these - // serializations is an opportunity to OOM and (depending on the site of - // the OOM), either crash, lose all data for the frame or lose all data - // for the application. - // - // In order to avoid this, compute an estimate of the size of the - // object, and block SessionStorage items that are too large. As - // we also don't want to cause an OOM here, we use a quick and memory- - // efficient approximation: we compute the total sum of string lengths - // involved in this object. - estimateStorageSize: function(collected) { - if (!collected) { - return 0; - } - - let size = 0; - for (let host of Object.keys(collected)) { - size += host.length; - let perHost = collected[host]; - for (let key of Object.keys(perHost)) { - size += key.length; - let perKey = perHost[key]; - size += perKey.length; - } - } - - return size; - }, - - // We don't want to send all the session storage data for all the frames - // for every change. So if only a few value changed we send them over as - // a "storagechange" event. If however for some reason before we send these - // changes we have to send over the entire sessions storage data, we just - // reset these changes. - _changes: undefined, - - resetChanges: function () { - this._changes = undefined; - }, - - collectFromEvent: function (event) { - // TODO: we should take browser.sessionstore.dom_storage_limit into an account here. - if (docShell) { - let {url, key, newValue} = event; - let uri = Services.io.newURI(url, null, null); - let domain = uri.prePath; - if (!this._changes) { - this._changes = {}; - } - if (!this._changes[domain]) { - this._changes[domain] = {}; - } - this._changes[domain][key] = newValue; - - MessageQueue.push("storagechange", () => { - let tmp = this._changes; - // If there were multiple changes we send them merged. - // First one will collect all the changes the rest of - // these messages will be ignored. - this.resetChanges(); - return tmp; - }); - } - }, - - collect: function () { - if (docShell) { - // We need the entire session storage, let's reset the pending individual change - // messages. - this.resetChanges(); - MessageQueue.push("storage", () => { - let collected = SessionStorage.collect(docShell, gFrameTree); - - if (collected == null) { - return collected; - } - - let size = this.estimateStorageSize(collected); - - MessageQueue.push("telemetry", () => ({ FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS: size })); - if (size > Preferences.get("browser.sessionstore.dom_storage_limit", DOM_STORAGE_MAX_CHARS)) { - // Rather than keeping the old storage, which wouldn't match the rest - // of the state of the page, empty the storage. DOM storage will be - // recollected the next time and stored if it is now small enough. - return {}; - } - - return collected; - }); - } - }, - - onFrameTreeCollected: function () { - this.collect(); - }, - - onFrameTreeReset: function () { - this.collect(); - } -}; - -/** - * Listen for changes to the privacy status of the tab. - * By definition, tabs start in non-private mode. - * - * Causes a SessionStore:update message to be sent for - * field "isPrivate". This message contains - * |true| if the tab is now private - * |null| if the tab is now public - the field is therefore - * not saved. - */ -var PrivacyListener = { - init: function() { - docShell.addWeakPrivacyTransitionObserver(this); - - // Check that value at startup as it might have - // been set before the frame script was loaded. - if (docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) { - MessageQueue.push("isPrivate", () => true); - } - }, - - // Ci.nsIPrivacyTransitionObserver - privateModeChanged: function(enabled) { - MessageQueue.push("isPrivate", () => enabled || null); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivacyTransitionObserver, - Ci.nsISupportsWeakReference]) -}; - -/** - * A message queue that takes collected data and will take care of sending it - * to the chrome process. It allows flushing using synchronous messages and - * takes care of any race conditions that might occur because of that. Changes - * will be batched if they're pushed in quick succession to avoid a message - * flood. - */ -var MessageQueue = { - /** - * A map (string -> lazy fn) holding lazy closures of all queued data - * collection routines. These functions will return data collected from the - * docShell. - */ - _data: new Map(), - - /** - * The delay (in ms) used to delay sending changes after data has been - * invalidated. - */ - BATCH_DELAY_MS: 1000, - - /** - * The current timeout ID, null if there is no queue data. We use timeouts - * to damp a flood of data changes and send lots of changes as one batch. - */ - _timeout: null, - - /** - * Whether or not sending batched messages on a timer is disabled. This should - * only be used for debugging or testing. If you need to access this value, - * you should probably use the timeoutDisabled getter. - */ - _timeoutDisabled: false, - - /** - * True if batched messages are not being fired on a timer. This should only - * ever be true when debugging or during tests. - */ - get timeoutDisabled() { - return this._timeoutDisabled; - }, - - /** - * Disables sending batched messages on a timer. Also cancels any pending - * timers. - */ - set timeoutDisabled(val) { - this._timeoutDisabled = val; - - if (val && this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - } - - return val; - }, - - init() { - this.timeoutDisabled = - Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF); - - Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this, false); - }, - - uninit() { - Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this); - }, - - observe(subject, topic, data) { - if (topic == "nsPref:changed" && data == TIMEOUT_DISABLED_PREF) { - this.timeoutDisabled = - Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF); - } - }, - - /** - * Pushes a given |value| onto the queue. The given |key| represents the type - * of data that is stored and can override data that has been queued before - * but has not been sent to the parent process, yet. - * - * @param key (string) - * A unique identifier specific to the type of data this is passed. - * @param fn (function) - * A function that returns the value that will be sent to the parent - * process. - */ - push: function (key, fn) { - this._data.set(key, createLazy(fn)); - - if (!this._timeout && !this._timeoutDisabled) { - // Wait a little before sending the message to batch multiple changes. - this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS); - } - }, - - /** - * Sends queued data to the chrome process. - * - * @param options (object) - * {flushID: 123} to specify that this is a flush - * {isFinal: true} to signal this is the final message sent on unload - */ - send: function (options = {}) { - // Looks like we have been called off a timeout after the tab has been - // closed. The docShell is gone now and we can just return here as there - // is nothing to do. - if (!docShell) { - return; - } - - if (this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - } - - let flushID = (options && options.flushID) || 0; - - let durationMs = Date.now(); - - let data = {}; - let telemetry = {}; - for (let [key, func] of this._data) { - let value = func(); - if (key == "telemetry") { - for (let histogramId of Object.keys(value)) { - telemetry[histogramId] = value[histogramId]; - } - } else if (value || (key != "storagechange" && key != "historychange")) { - data[key] = value; - } - } - - this._data.clear(); - - durationMs = Date.now() - durationMs; - telemetry.FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS = durationMs; - - try { - // Send all data to the parent process. - sendAsyncMessage("SessionStore:update", { - data, telemetry, flushID, - isFinal: options.isFinal || false, - epoch: gCurrentEpoch - }); - } catch (ex if ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) { - let telemetry = { - FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM: 1 - }; - sendAsyncMessage("SessionStore:error", { - telemetry - }); - } - }, -}; - -EventListener.init(); -MessageListener.init(); -FormDataListener.init(); -PageStyleListener.init(); -SessionHistoryListener.init(); -SessionStorageListener.init(); -ScrollPositionListener.init(); -DocShellCapabilitiesListener.init(); -PrivacyListener.init(); -MessageQueue.init(); - -function handleRevivedTab() { - if (!content) { - removeEventListener("pagehide", handleRevivedTab); - return; - } - - if (content.document.documentURI.startsWith("about:tabcrashed")) { - if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { - // Sanity check - we'd better be loading this in a non-remote browser. - throw new Error("We seem to be navigating away from about:tabcrashed in " + - "a non-remote browser. This should really never happen."); - } - - removeEventListener("pagehide", handleRevivedTab); - - // Notify the parent. - sendAsyncMessage("SessionStore:crashedTabRevived"); - } -} - -// If we're browsing from the tab crashed UI to a blacklisted URI that keeps -// this browser non-remote, we'll handle that in a pagehide event. -addEventListener("pagehide", handleRevivedTab); - -addEventListener("unload", () => { - // Upon frameLoader destruction, send a final update message to - // the parent and flush all data currently held in the child. - MessageQueue.send({isFinal: true}); - - // If we're browsing from the tab crashed UI to a URI that causes the tab - // to go remote again, we catch this in the unload event handler, because - // swapping out the non-remote browser for a remote one in - // tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide - // event to be fired. - handleRevivedTab(); - - // Remove all registered nsIObservers. - PageStyleListener.uninit(); - SessionStorageListener.uninit(); - SessionHistoryListener.uninit(); - MessageQueue.uninit(); - - // Remove progress listeners. - gContentRestore.resetRestore(); - - // We don't need to take care of any gFrameTree observers as the gFrameTree - // will die with the content script. The same goes for the privacy transition - // observer that will die with the docShell when the tab is closed. -}); diff --git a/browser/components/sessionstore/jar.mn b/browser/components/sessionstore/jar.mn deleted file mode 100644 index 7e5bc07dc..000000000 --- a/browser/components/sessionstore/jar.mn +++ /dev/null @@ -1,8 +0,0 @@ -# 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/. - -browser.jar: -* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml) - content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js) - content/browser/content-sessionStore.js (content/content-sessionStore.js) diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build deleted file mode 100644 index 3117f02c7..000000000 --- a/browser/components/sessionstore/moz.build +++ /dev/null @@ -1,46 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -JAR_MANIFESTS += ['jar.mn'] - -XPIDL_SOURCES += [ - 'nsISessionStartup.idl', - 'nsISessionStore.idl', -] - -XPIDL_MODULE = 'sessionstore' - -EXTRA_COMPONENTS += [ - 'nsSessionStartup.js', - 'nsSessionStore.js', - 'nsSessionStore.manifest', -] - -EXTRA_JS_MODULES.sessionstore = [ - 'ContentRestore.jsm', - 'DocShellCapabilities.jsm', - 'FrameTree.jsm', - 'GlobalState.jsm', - 'PageStyle.jsm', - 'PrivacyFilter.jsm', - 'PrivacyLevel.jsm', - 'RecentlyClosedTabsAndWindowsMenuUtils.jsm', - 'RunState.jsm', - 'SessionCookies.jsm', - 'SessionFile.jsm', - 'SessionHistory.jsm', - 'SessionMigration.jsm', - 'SessionSaver.jsm', - 'SessionStorage.jsm', - 'SessionStore.jsm', - 'SessionWorker.js', - 'SessionWorker.jsm', - 'StartupPerformance.jsm', - 'TabAttributes.jsm', - 'TabState.jsm', - 'TabStateCache.jsm', - 'TabStateFlusher.jsm', -] diff --git a/browser/components/sessionstore/nsISessionStartup.idl b/browser/components/sessionstore/nsISessionStartup.idl deleted file mode 100644 index 2321ac310..000000000 --- a/browser/components/sessionstore/nsISessionStartup.idl +++ /dev/null @@ -1,66 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -/** - * nsISessionStore keeps track of the current browsing state - i.e. - * tab history, cookies, scroll state, form data, and window features - * - and allows to restore everything into one window. - */ - -[scriptable, uuid(934697e4-3807-47f8-b6c9-6caa8d83ccd1)] -interface nsISessionStartup: nsISupports -{ - /** - * Return a promise that is resolved once initialization - * is complete. - */ - readonly attribute jsval onceInitialized; - - // Get session state - readonly attribute jsval state; - - /** - * Determines whether there is a pending session restore. Should only be - * called after initialization has completed. - */ - boolean doRestore(); - - /** - * Determines whether automatic session restoration is enabled for this - * launch of the browser. This does not include crash restoration, and will - * return false if restoration will only be caused by a crash. - */ - boolean isAutomaticRestoreEnabled(); - - /** - * Returns whether we will restore a session that ends up replacing the - * homepage. The browser uses this to not start loading the homepage if - * we're going to stop its load anyway shortly after. - * - * This is meant to be an optimization for the average case that loading the - * session file finishes before we may want to start loading the default - * homepage. Should this be called before the session file has been read it - * will just return false. - */ - readonly attribute bool willOverrideHomepage; - - /** - * What type of session we're restoring. - * NO_SESSION There is no data available from the previous session - * RECOVER_SESSION The last session crashed. It will either be restored or - * about:sessionrestore will be shown. - * RESUME_SESSION The previous session should be restored at startup - * DEFER_SESSION The previous session is fine, but it shouldn't be restored - * without explicit action (with the exception of pinned tabs) - */ - const unsigned long NO_SESSION = 0; - const unsigned long RECOVER_SESSION = 1; - const unsigned long RESUME_SESSION = 2; - const unsigned long DEFER_SESSION = 3; - - readonly attribute unsigned long sessionType; - readonly attribute bool previousSessionCrashed; -}; diff --git a/browser/components/sessionstore/nsISessionStore.idl b/browser/components/sessionstore/nsISessionStore.idl deleted file mode 100644 index 0d2500ef7..000000000 --- a/browser/components/sessionstore/nsISessionStore.idl +++ /dev/null @@ -1,220 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "nsISupports.idl" - -interface nsIDOMWindow; -interface nsIDOMNode; - -/** - * nsISessionStore keeps track of the current browsing state - i.e. - * tab history, cookies, scroll state, form data, and window features - * - and allows to restore everything into one browser window. - * - * The nsISessionStore API operates mostly on browser windows and the tabbrowser - * tabs contained in them: - * - * * "Browser windows" are those DOM windows having loaded - * chrome://browser/content/browser.xul . From overlays you can just pass the - * global |window| object to the API, though (or |top| from a sidebar). - * From elsewhere you can get browser windows through the nsIWindowMediator - * by looking for "navigator:browser" windows. - * - * * "Tabbrowser tabs" are all the child nodes of a browser window's - * |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|. - */ - -[scriptable, uuid(4580f5eb-693d-423d-b0ce-2cb20a962e4d)] -interface nsISessionStore : nsISupports -{ - /** - * Is it possible to restore the previous session. Will always be false when - * in Private Browsing mode. - */ - attribute boolean canRestoreLastSession; - - /** - * Restore the previous session if possible. This will not overwrite the - * current session. Instead the previous session will be merged into the - * current session. Current windows will be reused if they were windows that - * pinned tabs were previously restored into. New windows will be opened as - * needed. - * - * Note: This will throw if there is no previous state to restore. Check with - * canRestoreLastSession first to avoid thrown errors. - */ - void restoreLastSession(); - - /** - * Get the current browsing state. - * @returns a JSON string representing the session state. - */ - AString getBrowserState(); - - /** - * Set the browsing state. - * This will immediately restore the state of the whole application to the state - * passed in, *replacing* the current session. - * - * @param aState is a JSON string representing the session state. - */ - void setBrowserState(in AString aState); - - /** - * @param aWindow is the browser window whose state is to be returned. - * - * @returns a JSON string representing a session state with only one window. - */ - AString getWindowState(in nsIDOMWindow aWindow); - - /** - * @param aWindow is the browser window whose state is to be set. - * @param aState is a JSON string representing a session state. - * @param aOverwrite boolean overwrite existing tabs - */ - void setWindowState(in nsIDOMWindow aWindow, in AString aState, in boolean aOverwrite); - - /** - * @param aTab is the tabbrowser tab whose state is to be returned. - * - * @returns a JSON string representing the state of the tab - * (note: doesn't contain cookies - if you need them, use getWindowState instead). - */ - AString getTabState(in nsIDOMNode aTab); - - /** - * @param aTab is the tabbrowser tab whose state is to be set. - * @param aState is a JSON string representing a session state. - */ - void setTabState(in nsIDOMNode aTab, in AString aState); - - /** - * Duplicates a given tab as thoroughly as possible. - * - * @param aWindow is the browser window into which the tab will be duplicated. - * @param aTab is the tabbrowser tab to duplicate (can be from a different window). - * @param aDelta is the offset to the history entry to load in the duplicated tab. - * @returns a reference to the newly created tab. - */ - nsIDOMNode duplicateTab(in nsIDOMWindow aWindow, in nsIDOMNode aTab, - [optional] in long aDelta); - - /** - * Get the number of restore-able tabs for a browser window - */ - unsigned long getClosedTabCount(in nsIDOMWindow aWindow); - - /** - * Get closed tab data - * - * @param aWindow is the browser window for which to get closed tab data - * @returns a JSON string representing the list of closed tabs. - */ - AString getClosedTabData(in nsIDOMWindow aWindow); - - /** - * @param aWindow is the browser window to reopen a closed tab in. - * @param aIndex is the index of the tab to be restored (FIFO ordered). - * @returns a reference to the reopened tab. - */ - nsIDOMNode undoCloseTab(in nsIDOMWindow aWindow, in unsigned long aIndex); - - /** - * @param aWindow is the browser window associated with the closed tab. - * @param aIndex is the index of the closed tab to be removed (FIFO ordered). - */ - nsIDOMNode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex); - - /** - * Get the number of restore-able windows - */ - unsigned long getClosedWindowCount(); - - /** - * Get closed windows data - * - * @returns a JSON string representing the list of closed windows. - */ - AString getClosedWindowData(); - - /** - * @param aIndex is the index of the windows to be restored (FIFO ordered). - * @returns the nsIDOMWindow object of the reopened window - */ - nsIDOMWindow undoCloseWindow(in unsigned long aIndex); - - /** - * @param aIndex is the index of the closed window to be removed (FIFO ordered). - * - * @throws NS_ERROR_INVALID_ARG - * when aIndex does not map to a closed window - */ - nsIDOMNode forgetClosedWindow(in unsigned long aIndex); - - /** - * @param aWindow is the window to get the value for. - * @param aKey is the value's name. - * - * @returns A string value or an empty string if none is set. - */ - AString getWindowValue(in nsIDOMWindow aWindow, in AString aKey); - - /** - * @param aWindow is the browser window to set the value for. - * @param aKey is the value's name. - * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects). - */ - void setWindowValue(in nsIDOMWindow aWindow, in AString aKey, in jsval aStringValue); - - /** - * @param aWindow is the browser window to get the value for. - * @param aKey is the value's name. - */ - void deleteWindowValue(in nsIDOMWindow aWindow, in AString aKey); - - /** - * @param aTab is the tabbrowser tab to get the value for. - * @param aKey is the value's name. - * - * @returns A string value or an empty string if none is set. - */ - AString getTabValue(in nsIDOMNode aTab, in AString aKey); - - /** - * @param aTab is the tabbrowser tab to set the value for. - * @param aKey is the value's name. - * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects). - */ - void setTabValue(in nsIDOMNode aTab, in AString aKey, in jsval aStringValue); - - /** - * @param aTab is the tabbrowser tab to get the value for. - * @param aKey is the value's name. - */ - void deleteTabValue(in nsIDOMNode aTab, in AString aKey); - - /** - * @param aKey is the value's name. - * - * @returns A string value or an empty string if none is set. - */ - AString getGlobalValue(in AString aKey); - - /** - * @param aKey is the value's name. - * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects). - */ - void setGlobalValue(in AString aKey, in jsval aStringValue); - - /** - * @param aTab is the browser tab to get the value for. - * @param aKey is the value's name. - */ - void deleteGlobalValue(in AString aKey); - - /** - * @param aName is the name of the attribute to save/restore for all tabbrowser tabs. - */ - void persistTabAttribute(in AString aName); -}; diff --git a/browser/components/sessionstore/nsSessionStartup.js b/browser/components/sessionstore/nsSessionStartup.js deleted file mode 100644 index 7593c48ec..000000000 --- a/browser/components/sessionstore/nsSessionStartup.js +++ /dev/null @@ -1,353 +0,0 @@ -/* 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"; - -/** - * Session Storage and Restoration - * - * Overview - * This service reads user's session file at startup, and makes a determination - * as to whether the session should be restored. It will restore the session - * under the circumstances described below. If the auto-start Private Browsing - * mode is active, however, the session is never restored. - * - * Crash Detection - * The CrashMonitor is used to check if the final session state was successfully - * written at shutdown of the last session. If we did not reach - * 'sessionstore-final-state-write-complete', then it's assumed that the browser - * has previously crashed and we should restore the session. - * - * Forced Restarts - * In the event that a restart is required due to application update or extension - * installation, set the browser.sessionstore.resume_session_once pref to true, - * and the session will be restored the next time the browser starts. - * - * Always Resume - * This service will always resume the session if the integer pref - * browser.startup.page is set to 3. - */ - -/* :::::::: Constants and Helpers ::::::::::::::: */ - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/TelemetryStopwatch.jsm"); -Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "console", - "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", - "resource:///modules/sessionstore/SessionFile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "StartupPerformance", - "resource:///modules/sessionstore/StartupPerformance.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor", - "resource://gre/modules/CrashMonitor.jsm"); - -const STATE_RUNNING_STR = "running"; - -// 'browser.startup.page' preference value to resume the previous session. -const BROWSER_STARTUP_RESUME_SESSION = 3; - -function debug(aMsg) { - aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n"); - Services.console.logStringMessage(aMsg); -} -function warning(aMsg, aException) { - let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError); -consoleMsg.init(aMsg, aException.fileName, null, aException.lineNumber, 0, Ci.nsIScriptError.warningFlag, "component javascript"); - Services.console.logMessage(consoleMsg); -} - -var gOnceInitializedDeferred = (function () { - let deferred = {}; - - deferred.promise = new Promise((resolve, reject) => { - deferred.resolve = resolve; - deferred.reject = reject; - }); - - return deferred; -})(); - -/* :::::::: The Service ::::::::::::::: */ - -function SessionStartup() { -} - -SessionStartup.prototype = { - - // the state to restore at startup - _initialState: null, - _sessionType: Ci.nsISessionStartup.NO_SESSION, - _initialized: false, - - // Stores whether the previous session crashed. - _previousSessionCrashed: null, - -/* ........ Global Event Handlers .............. */ - - /** - * Initialize the component - */ - init: function sss_init() { - Services.obs.notifyObservers(null, "sessionstore-init-started", null); - StartupPerformance.init(); - - // do not need to initialize anything in auto-started private browsing sessions - if (PrivateBrowsingUtils.permanentPrivateBrowsing) { - this._initialized = true; - gOnceInitializedDeferred.resolve(); - return; - } - - SessionFile.read().then( - this._onSessionFileRead.bind(this), - console.error - ); - }, - - // Wrap a string as a nsISupports - _createSupportsString: function ssfi_createSupportsString(aData) { - let string = Cc["@mozilla.org/supports-string;1"] - .createInstance(Ci.nsISupportsString); - string.data = aData; - return string; - }, - - /** - * Complete initialization once the Session File has been read - * - * @param source The Session State string read from disk. - * @param parsed The object obtained by parsing |source| as JSON. - */ - _onSessionFileRead: function ({source, parsed, noFilesFound}) { - this._initialized = true; - - // Let observers modify the state before it is used - let supportsStateString = this._createSupportsString(source); - Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", ""); - let stateString = supportsStateString.data; - - if (stateString != source) { - // The session has been modified by an add-on, reparse. - try { - this._initialState = JSON.parse(stateString); - } catch (ex) { - // That's not very good, an add-on has rewritten the initial - // state to something that won't parse. - warning("Observer rewrote the state to something that won't parse", ex); - } - } else { - // No need to reparse - this._initialState = parsed; - } - - if (this._initialState == null) { - // No valid session found. - this._sessionType = Ci.nsISessionStartup.NO_SESSION; - Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); - gOnceInitializedDeferred.resolve(); - return; - } - - let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); - let shouldResumeSession = shouldResumeSessionOnce || - Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION; - - // If this is a normal restore then throw away any previous session - if (!shouldResumeSessionOnce && this._initialState) { - delete this._initialState.lastSessionState; - } - - let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"); - - CrashMonitor.previousCheckpoints.then(checkpoints => { - if (checkpoints) { - // If the previous session finished writing the final state, we'll - // assume there was no crash. - this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"]; - - } else { - // If the Crash Monitor could not load a checkpoints file it will - // provide null. This could occur on the first run after updating to - // a version including the Crash Monitor, or if the checkpoints file - // was removed, or on first startup with this profile, or after Firefox Reset. - - if (noFilesFound) { - // There was no checkpoints file and no sessionstore.js or its backups - // so we will assume that this was a fresh profile. - this._previousSessionCrashed = false; - - } else { - // If this is the first run after an update, sessionstore.js should - // still contain the session.state flag to indicate if the session - // crashed. If it is not present, we will assume this was not the first - // run after update and the checkpoints file was somehow corrupted or - // removed by a crash. - // - // If the session.state flag is present, we will fallback to using it - // for crash detection - If the last write of sessionstore.js had it - // set to "running", we crashed. - let stateFlagPresent = (this._initialState.session && - this._initialState.session.state); - - - this._previousSessionCrashed = !stateFlagPresent || - (this._initialState.session.state == STATE_RUNNING_STR); - } - } - - // Report shutdown success via telemetry. Shortcoming here are - // being-killed-by-OS-shutdown-logic, shutdown freezing after - // session restore was written, etc. - Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!this._previousSessionCrashed); - - // set the startup type - if (this._previousSessionCrashed && resumeFromCrash) - this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION; - else if (!this._previousSessionCrashed && shouldResumeSession) - this._sessionType = Ci.nsISessionStartup.RESUME_SESSION; - else if (this._initialState) - this._sessionType = Ci.nsISessionStartup.DEFER_SESSION; - else - this._initialState = null; // reset the state - - Services.obs.addObserver(this, "sessionstore-windows-restored", true); - - if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) - Services.obs.addObserver(this, "browser:purge-session-history", true); - - // We're ready. Notify everyone else. - Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); - gOnceInitializedDeferred.resolve(); - }); - }, - - /** - * Handle notifications - */ - observe: function sss_observe(aSubject, aTopic, aData) { - switch (aTopic) { - case "app-startup": - Services.obs.addObserver(this, "final-ui-startup", true); - Services.obs.addObserver(this, "quit-application", true); - break; - case "final-ui-startup": - Services.obs.removeObserver(this, "final-ui-startup"); - Services.obs.removeObserver(this, "quit-application"); - this.init(); - break; - case "quit-application": - // no reason for initializing at this point (cf. bug 409115) - Services.obs.removeObserver(this, "final-ui-startup"); - Services.obs.removeObserver(this, "quit-application"); - if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) - Services.obs.removeObserver(this, "browser:purge-session-history"); - break; - case "sessionstore-windows-restored": - Services.obs.removeObserver(this, "sessionstore-windows-restored"); - // free _initialState after nsSessionStore is done with it - this._initialState = null; - break; - case "browser:purge-session-history": - Services.obs.removeObserver(this, "browser:purge-session-history"); - // reset all state on sanitization - this._sessionType = Ci.nsISessionStartup.NO_SESSION; - break; - } - }, - -/* ........ Public API ................*/ - - get onceInitialized() { - return gOnceInitializedDeferred.promise; - }, - - /** - * Get the session state as a jsval - */ - get state() { - return this._initialState; - }, - - /** - * Determines whether there is a pending session restore. Should only be - * called after initialization has completed. - * @returns bool - */ - doRestore: function sss_doRestore() { - return this._willRestore(); - }, - - /** - * Determines whether automatic session restoration is enabled for this - * launch of the browser. This does not include crash restoration. In - * particular, if session restore is configured to restore only in case of - * crash, this method returns false. - * @returns bool - */ - isAutomaticRestoreEnabled: function () { - return Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") || - Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION; - }, - - /** - * Determines whether there is a pending session restore. - * @returns bool - */ - _willRestore: function () { - return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION || - this._sessionType == Ci.nsISessionStartup.RESUME_SESSION; - }, - - /** - * Returns whether we will restore a session that ends up replacing the - * homepage. The browser uses this to not start loading the homepage if - * we're going to stop its load anyway shortly after. - * - * This is meant to be an optimization for the average case that loading the - * session file finishes before we may want to start loading the default - * homepage. Should this be called before the session file has been read it - * will just return false. - * - * @returns bool - */ - get willOverrideHomepage() { - if (this._initialState && this._willRestore()) { - let windows = this._initialState.windows || null; - // If there are valid windows with not only pinned tabs, signal that we - // will override the default homepage by restoring a session. - return windows && windows.some(w => w.tabs.some(t => !t.pinned)); - } - return false; - }, - - /** - * Get the type of pending session store, if any. - */ - get sessionType() { - return this._sessionType; - }, - - /** - * Get whether the previous session crashed. - */ - get previousSessionCrashed() { - return this._previousSessionCrashed; - }, - - /* ........ QueryInterface .............. */ - QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference, - Ci.nsISessionStartup]), - classID: Components.ID("{ec7a6c20-e081-11da-8ad9-0800200c9a66}") -}; - -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]); diff --git a/browser/components/sessionstore/nsSessionStore.js b/browser/components/sessionstore/nsSessionStore.js deleted file mode 100644 index 8d96178ce..000000000 --- a/browser/components/sessionstore/nsSessionStore.js +++ /dev/null @@ -1,39 +0,0 @@ -/* 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"; - -/** - * Session Storage and Restoration - * - * Overview - * This service keeps track of a user's session, storing the various bits - * required to return the browser to its current state. The relevant data is - * stored in memory, and is periodically saved to disk in a file in the - * profile directory. The service is started at first window load, in - * delayedStartup, and will restore the session from the data received from - * the nsSessionStartup service. - */ - -const Cu = Components.utils; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource:///modules/sessionstore/SessionStore.jsm"); - -function SessionStoreService() {} - -// The SessionStore module's object is frozen. We need to modify our prototype -// and add some properties so let's just copy the SessionStore object. -Object.keys(SessionStore).forEach(function (aName) { - let desc = Object.getOwnPropertyDescriptor(SessionStore, aName); - Object.defineProperty(SessionStoreService.prototype, aName, desc); -}); - -SessionStoreService.prototype.classID = - Components.ID("{5280606b-2510-4fe0-97ef-9b5a22eafe6b}"); -SessionStoreService.prototype.QueryInterface = - XPCOMUtils.generateQI([Ci.nsISessionStore]); - -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStoreService]); diff --git a/browser/components/sessionstore/nsSessionStore.manifest b/browser/components/sessionstore/nsSessionStore.manifest deleted file mode 100644 index 9b5819c6a..000000000 --- a/browser/components/sessionstore/nsSessionStore.manifest +++ /dev/null @@ -1,15 +0,0 @@ -# This component must restrict its registration for the app-startup category -# to the specific list of apps that use it so it doesn't get loaded in xpcshell. -# Thus we restrict it to these apps: -# -# b2g: {3c2e2abc-06d4-11e1-ac3b-374f68613e61} -# browser: {ec8030f7-c20a-464f-9b0e-13a3a9e97384} -# mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110} -# mobile/xul: {a23983c0-fd0e-11dc-95ff-0800200c9a66} -# graphene: {d1bfe7d9-c01e-4237-998b-7b5f960a4314} - -component {5280606b-2510-4fe0-97ef-9b5a22eafe6b} nsSessionStore.js -contract @mozilla.org/browser/sessionstore;1 {5280606b-2510-4fe0-97ef-9b5a22eafe6b} -component {ec7a6c20-e081-11da-8ad9-0800200c9a66} nsSessionStartup.js -contract @mozilla.org/browser/sessionstartup;1 {ec7a6c20-e081-11da-8ad9-0800200c9a66} -category app-startup nsSessionStartup service,@mozilla.org/browser/sessionstartup;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={d1bfe7d9-c01e-4237-998b-7b5f960a4314} |