diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/components/sessionstore | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/suite/components/sessionstore')
10 files changed, 5166 insertions, 0 deletions
diff --git a/comm/suite/components/sessionstore/XPathGenerator.jsm b/comm/suite/components/sessionstore/XPathGenerator.jsm new file mode 100644 index 0000000000..e202468a27 --- /dev/null +++ b/comm/suite/components/sessionstore/XPathGenerator.jsm @@ -0,0 +1,97 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = ["XPathGenerator"]; + +var XPathGenerator = { + // these two hashes should be kept in sync + namespaceURIs: { "xhtml": "http://www.w3.org/1999/xhtml" }, + namespacePrefixes: { "http://www.w3.org/1999/xhtml": "xhtml" }, + + /** + * Generates an approximate XPath query to an (X)HTML node + */ + generate: function sss_xph_generate(aNode) { + // have we reached the document node already? + if (!aNode.parentNode) + return ""; + + // Access localName, namespaceURI just once per node since it's expensive. + let nNamespaceURI = aNode.namespaceURI; + let nLocalName = aNode.localName; + + let prefix = this.namespacePrefixes[nNamespaceURI] || null; + let tag = (prefix ? prefix + ":" : "") + this.escapeName(nLocalName); + + // stop once we've found a tag with an ID + if (aNode.id) + return "//" + tag + "[@id=" + this.quoteArgument(aNode.id) + "]"; + + // count the number of previous sibling nodes of the same tag + // (and possible also the same name) + let count = 0; + let nName = aNode.name || null; + for (let n = aNode; (n = n.previousSibling); ) + if (n.localName == nLocalName && n.namespaceURI == nNamespaceURI && + (!nName || n.name == nName)) + count++; + + // recurse until hitting either the document node or an ID'd node + return this.generate(aNode.parentNode) + "/" + tag + + (nName ? "[@name=" + this.quoteArgument(nName) + "]" : "") + + (count ? "[" + (count + 1) + "]" : ""); + }, + + /** + * Resolves an XPath query generated by XPathGenerator.generate + */ + resolve: function sss_xph_resolve(aDocument, aQuery) { + let xptype = aDocument.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE; + return aDocument.evaluate(aQuery, aDocument, this.resolveNS, xptype, null).singleNodeValue; + }, + + /** + * Namespace resolver for the above XPath resolver + */ + resolveNS: function sss_xph_resolveNS(aPrefix) { + return XPathGenerator.namespaceURIs[aPrefix] || null; + }, + + /** + * @returns valid XPath for the given node (usually just the local name itself) + */ + escapeName: function sss_xph_escapeName(aName) { + // we can't just use the node's local name, if it contains + // special characters (cf. bug 485482) + return /^\w+$/.test(aName) ? aName : + "*[local-name()=" + this.quoteArgument(aName) + "]"; + }, + + /** + * @returns a properly quoted string to insert into an XPath query + */ + quoteArgument: function sss_xph_quoteArgument(aArg) { + return !/'/.test(aArg) ? "'" + aArg + "'" : + !/"/.test(aArg) ? '"' + aArg + '"' : + "concat('" + aArg.replace(/'+/g, "',\"$&\",'") + "')"; + }, + + /** + * @returns an XPath query to all savable form field nodes + */ + get restorableFormNodes() { + // for a comprehensive list of all available <INPUT> types see + // http://mxr.mozilla.org/mozilla-central/search?string=kInputTypeTable + let ignoreTypes = ["password", "hidden", "button", "image", "submit", "reset"]; + // XXXzeniko work-around until lower-case has been implemented (bug 398389) + let toLowerCase = '"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"'; + let ignore = "not(translate(@type, " + toLowerCase + ")='" + + ignoreTypes.join("' or translate(@type, " + toLowerCase + ")='") + "')"; + let formNodesXPath = "//textarea|//select|//xhtml:textarea|//xhtml:select|" + + "//input[" + ignore + "]|//xhtml:input[" + ignore + "]"; + + delete this.restorableFormNodes; + return (this.restorableFormNodes = formNodesXPath); + } +}; diff --git a/comm/suite/components/sessionstore/content/aboutSessionRestore.js b/comm/suite/components/sessionstore/content/aboutSessionRestore.js new file mode 100644 index 0000000000..677bf20adc --- /dev/null +++ b/comm/suite/components/sessionstore/content/aboutSessionRestore.js @@ -0,0 +1,291 @@ +/* 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/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var gStateObject; +var gTreeData; + +// Page initialization + +window.onload = function() { + // establish the event handlers for <tree> and <button> elements + var tabList = document.getElementById("tabList"); + tabList.addEventListener("click", onListClick); + tabList.addEventListener("keydown", onListKeyDown); + + document.getElementById("errorTryAgain") + .addEventListener("command", restoreSession); + + document.getElementById("errorCancel") + .addEventListener("command", startNewSession); + + // 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) { + var ss = Cc["@mozilla.org/suite/sessionstartup;1"].getService(Ci.nsISessionStartup); + sessionData.value = ss.state; + if (!sessionData.value) { + document.getElementById("errorTryAgain").disabled = true; + return; + } + } + // make sure the data is tracked to be restored in case of a subsequent crash + sessionData.dispatchEvent(new UIEvent("input", + { bubbles: true, cancelable: true, view: window, detail: 0 })); + + gStateObject = JSON.parse(sessionData.value); + + initTreeView(); + + document.getElementById("errorTryAgain").focus(); +}; + +function initTreeView() { + var tabList = document.getElementById("tabList"); + var winLabel = tabList.getAttribute("_window_label"); + + gTreeData = []; + 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.attributes && aTabData.attributes.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 (var tab of winState.tabs) + gTreeData.push(tab); + }, this); + + tabList.view = treeView; + tabList.view.selection.select(0); +} + +// User actions + +function restoreSession() { + document.getElementById("errorTryAgain").disabled = true; + + // 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/suite/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.tabContainer.childNodes.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", "about:blank"); + var tab = top.gBrowser.selectedTab; + newWindow.addEventListener("load", function newWindowLoad() { + newWindow.removeEventListener("load", newWindowLoad, true); + ss.setWindowState(newWindow, stateString, true); + + top.gBrowser.removeTab(tab); + }, true); +} + +function startNewSession() { + if (Services.prefs.getIntPref("browser.startup.page") == 1) + getBrowserWindow().BrowserHome(); + else + getBrowserWindow().getBrowser().loadURI("about:blank"); +} + +function onListClick(aEvent) { + // don't react to right-clicks + if (aEvent.button == 2) + return; + + var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY); + if (cell.col) { + // restore this specific tab in the same window for middle-clicking + // or Ctrl+clicking on a tab's title + if ((aEvent.button == 1 || aEvent.ctrlKey) && cell.col.id == "title" && + !treeView.isContainer(cell.row)) + restoreSingleTab(cell.row, aEvent.shiftKey); + 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); + 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) { + var item = gTreeData[aIx]; + item.checked = !item.checked; + treeView.treeBox.invalidateRow(aIx); + + function isChecked(aItem) { + return aItem.checked; + } + + if (treeView.isContainer(aIx)) { + // (un)check all tabs of this window as well + for (var 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)); + } + + 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/suite/sessionstore;1"].getService(Ci.nsISessionStore); + var tabState = gStateObject.windows[item.parent.ix] + .tabs[aIx - gTreeData.indexOf(item.parent) - 1]; + ss.setTabState(newTab, JSON.stringify(tabState)); + + // respect the preference as to whether to select the tab (the Shift key inverses) + if (Services.prefs.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 "open" in gTreeData[idx]; }, + 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]; + return winState.ix % 2 != 0 ? "alternate" : ""; + }, + + 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() { }, + getColumnProperties: function(column) { return ""; } +}; diff --git a/comm/suite/components/sessionstore/content/aboutSessionRestore.xhtml b/comm/suite/components/sessionstore/content/aboutSessionRestore.xhtml new file mode 100644 index 0000000000..34c9a893ea --- /dev/null +++ b/comm/suite/components/sessionstore/content/aboutSessionRestore.xhtml @@ -0,0 +1,84 @@ +<?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 % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > + %brandDTD; + <!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://communicator/locale/aboutSessionRestore.dtd"> + %restorepageDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&restorepage.tabtitle;</title> + <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all"/> + <link rel="stylesheet" href="chrome://communicator/skin/aboutSessionRestore.css" type="text/css" media="all"/> + <link rel="icon" type="image/png" href="chrome://global/skin/icons/question-16.png"/> + + <script src="chrome://communicator/content/aboutSessionRestore.js"/> + </head> + + <body dir="&locale.dir;"> + + <!-- PAGE CONTAINER (for styling purposes only) --> + <div id="errorPageContainer"> + + <!-- Error Title --> + <div id="errorTitle"> + <h1 id="errorTitleText">&restorepage.pagetitle;</h1> + </div> + + <!-- LONG CONTENT (the section most likely to require scrolling) --> + <div id="errorLongContent"> + + <!-- Short Description --> + <div id="errorShortDesc"> + <p id="errorShortDescText">&restorepage.issueDesc;</p> + </div> + + <!-- Long Description (Note: See netError.dtd for used XHTML tags) --> + <div id="errorLongDesc"> + <p>&restorepage.remedies;</p> + <ul> + <li>&restorepage.dueToChrome;</li> + <li>&restorepage.dueToContent;</li> + </ul> + </div> + + <!-- Short Description --> + <div id="errorTrailerDesc"> + <tree xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="tabList" flex="1" seltype="single" hidecolumnpicker="true" + _window_label="&restorepage.windowLabel;"> + <treecols> + <treecol id="restore" type="checkbox" label="&restorepage.restoreHeader;"/> + <splitter class="tree-splitter"/> + <treecol primary="true" id="title" label="&restorepage.listHeader;" flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + </div> + </div> + + <!-- Buttons --> + <hbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" id="buttons"> + <button id="errorTryAgain" label="&restorepage.restoreButton;" + accesskey="&restorepage.restore.access;"/> + <button id="errorCancel" label="&restorepage.cancelButton;" + accesskey="&restorepage.cancel.access;"/> + </hbox> + <!-- holds the session data for when the tab is closed --> + <input type="hidden" id="sessionData"/> + </div> + + </body> +</html> diff --git a/comm/suite/components/sessionstore/jar.mn b/comm/suite/components/sessionstore/jar.mn new file mode 100644 index 0000000000..5a398c353e --- /dev/null +++ b/comm/suite/components/sessionstore/jar.mn @@ -0,0 +1,7 @@ +# 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/. + +comm.jar: + content/communicator/aboutSessionRestore.js (content/aboutSessionRestore.js) + content/communicator/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml) diff --git a/comm/suite/components/sessionstore/moz.build b/comm/suite/components/sessionstore/moz.build new file mode 100644 index 0000000000..9d8fd29468 --- /dev/null +++ b/comm/suite/components/sessionstore/moz.build @@ -0,0 +1,24 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + "nsISessionStartup.idl", + "nsISessionStore.idl", +] + +XPIDL_MODULE = "suitecommon" + +EXTRA_COMPONENTS += [ + "nsSessionStartup.js", + "nsSessionStartup.manifest", + "nsSessionStore.js", +] + +EXTRA_JS_MODULES.sessionstore = [ + "XPathGenerator.jsm", +] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/comm/suite/components/sessionstore/nsISessionStartup.idl b/comm/suite/components/sessionstore/nsISessionStartup.idl new file mode 100644 index 0000000000..018b0c1d4c --- /dev/null +++ b/comm/suite/components/sessionstore/nsISessionStartup.idl @@ -0,0 +1,39 @@ +/* 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, POSTDATA and window features + * - and allows to restore everything into one window. + */ + +[scriptable, uuid(dd709821-820a-4d0d-b7e8-a566b32377ef)] +interface nsISessionStartup: nsISupports +{ + // Get session state + readonly attribute jsval state; + + /** + * Determine if session should be restored + */ + boolean doRestore(); + + /** + * 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; +}; diff --git a/comm/suite/components/sessionstore/nsISessionStore.idl b/comm/suite/components/sessionstore/nsISessionStore.idl new file mode 100644 index 0000000000..2fc2f22175 --- /dev/null +++ b/comm/suite/components/sessionstore/nsISessionStore.idl @@ -0,0 +1,216 @@ +/* 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 nsINode; + +/** + * nsISessionStore keeps track of the current browsing state - i.e. + * tab history, cookies, scroll state, form data, POSTDATA 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://navigator/content/navigator.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 + * |getBrowser().tabContainer| such as e.g. |getBrowser().selectedTab|. + */ + +[scriptable, uuid(27a8bd2b-dd76-4cee-82eb-a25f6a94478f)] +interface nsISessionStore : nsISupports +{ + /** + * Initialize the service + */ + void init(in nsIDOMWindow aWindow); + + /** + * 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 nsINode 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 nsINode 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. + * Pass null if you want to create a new window. + * @param aTab is the tabbrowser tab to duplicate (can be from a different window). + * @param aDelta is the offset to the history entry that you want to load. + * @param aRelated is a flag to be passed to addTab(). + * @returns a reference to the newly created tab, or null if opening a window. + */ + nsINode duplicateTab(in nsIDOMWindow aWindow, in nsINode aTab, + [optional] in long aDelta, + [optional] in boolean aRelated); + + /** + * 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. + */ + nsINode 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). + */ + nsINode 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 + */ + nsINode 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 toSource/eval before setting JS objects). + */ + void setWindowValue(in nsIDOMWindow aWindow, in AString aKey, in AString 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 nsINode 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 toSource/eval before setting JS objects). + */ + void setTabValue(in nsINode aTab, in AString aKey, in AString aStringValue); + + /** + * @param aTab is the tabbrowser tab to get the value for. + * @param aKey is the value's name. + */ + void deleteTabValue(in nsINode aTab, in AString aKey); + + /** + * @param aName is the name of the attribute to save/restore for all tabbrowser tabs. + */ + void persistTabAttribute(in AString aName); + + /** + * Returns true if the last window was closed and should be restored + * + * @returns true if the last window was closed and should be restored + */ + boolean doRestoreLastWindow(); +}; diff --git a/comm/suite/components/sessionstore/nsSessionStartup.js b/comm/suite/components/sessionstore/nsSessionStartup.js new file mode 100644 index 0000000000..32494d1f27 --- /dev/null +++ b/comm/suite/components/sessionstore/nsSessionStartup.js @@ -0,0 +1,223 @@ +/* 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/. */ + +/** + * 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. + * + * Crash Detection + * The session file stores a session.state property, that + * indicates whether the browser is currently running. When the browser shuts + * down, the field is changed to "stopped". At startup, this field is read, and + * if it's value is "running", then it's assumed that the browser had previously + * crashed, or at the very least that something bad happened, and that 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 ::::::::::::::: */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const STATE_RUNNING_STR = "running"; + +function debug(aMsg) { + Services.console.logStringMessage("SessionStartup: " + aMsg); +} + +/* :::::::: The Service ::::::::::::::: */ + +function SessionStartup() { +} + +SessionStartup.prototype = { + + // the state to restore at startup + _initialState: null, + _sessionType: Ci.nsISessionStartup.NO_SESSION, + +/* ........ Global Event Handlers .............. */ + + /** + * Initialize the component + */ + init: function sss_init() { + // get file references + let sessionFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + sessionFile.append("sessionstore.json"); + + let doResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); + let doResumeSession = doResumeSessionOnce || + Services.prefs.getIntPref("browser.startup.page") == 3; + + var resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"); + + // only continue if the session file exists + if (!sessionFile.exists()) + return; + + // get string containing session state + let iniString = this._readStateFile(sessionFile); + if (!iniString) + return; + + try { + // parse the session state into JS objects + this._initialState = JSON.parse(iniString); + } + catch (ex) { + doResumeSession = false; + debug("The session file is invalid: " + ex); + } + + // If this is a normal restore then throw away any previous session + if (!doResumeSessionOnce && this._initialState) + delete this._initialState.lastSessionState; + + let lastSessionCrashed = + this._initialState && this._initialState.session && + this._initialState.session.state && + this._initialState.session.state == STATE_RUNNING_STR; + + // set the startup type + if (lastSessionCrashed && resumeFromCrash) + this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION; + else if (!lastSessionCrashed && doResumeSession) + this._sessionType = Ci.nsISessionStartup.RESUME_SESSION; + else if (this._initialState) + this._sessionType = Ci.nsISessionStartup.DEFER_SESSION; + else + this._initialState = null; // reset the state + + if (this.doRestore()) { + // wait for the first browser window to open + Services.obs.addObserver(this, "sessionstore-windows-restored", true); + } + }, + + /** + * 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"); + break; + case "sessionstore-windows-restored": + // no need in repeating this, since session type won't change + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + // free _initialState after nsSessionStore is done with it + this._initialState = null; + // reset session type after restore + this._sessionType = Ci.nsISessionStartup.NO_SESSION; + break; + } + }, + +/* ........ Public API ................*/ + + /** + * Get the session state as a string + */ + get state() { + return this._initialState; + }, + + /** + * Determine whether there is a pending session restore. + * @returns bool + */ + doRestore: function sss_doRestore() { + return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION || + this._sessionType == Ci.nsISessionStartup.RESUME_SESSION; + }, + + /** + * Get the type of pending session store, if any. + */ + get sessionType() { + return this._sessionType; + }, + +/* ........ Storage API .............. */ + + /** + * Reads a session state file into a string and lets + * observers modify the state before it's being used + * + * @param aFile is any nsIFile + * @returns a session state string + */ + _readStateFile: function sss_readStateFile(aFile) { + var stateString = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + stateString.data = this._readFile(aFile) || ""; + + Services.obs.notifyObservers(stateString, "sessionstore-state-read"); + + return stateString.data; + }, + + /** + * reads a file into a string + * @param aFile + * nsIFile + * @returns string + */ + _readFile: function sss_readFile(aFile) { + try { + var stream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + stream.init(aFile, 0x01, 0, 0); + var cvStream = Cc["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Ci.nsIConverterInputStream); + cvStream.init(stream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + var content = ""; + var data = {}; + while (cvStream.readString(4096, data)) { + content += data.value; + } + cvStream.close(); + + return content.replace(/\r\n?/g, "\n"); + } + catch (ex) { Cu.reportError(ex); } + + return null; + }, + + /* ........ QueryInterface .............. */ + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference, + Ci.nsISessionStartup]), + classID: Components.ID("{4e6c1112-57b6-44ba-adf9-99fb573b0a30}") + +}; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]); diff --git a/comm/suite/components/sessionstore/nsSessionStartup.manifest b/comm/suite/components/sessionstore/nsSessionStartup.manifest new file mode 100644 index 0000000000..c1ade9aa87 --- /dev/null +++ b/comm/suite/components/sessionstore/nsSessionStartup.manifest @@ -0,0 +1,11 @@ +# This components 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: +# +# suite {4e6c1112-57b6-44ba-adf9-99fb573b0a30} + +component {4e6c1112-57b6-44ba-adf9-99fb573b0a30} nsSessionStartup.js +contract @mozilla.org/suite/sessionstartup;1 {4e6c1112-57b6-44ba-adf9-99fb573b0a30} +category app-startup SessionStartup service,@mozilla.org/suite/sessionstartup;1 +component {d37ccdf1-496f-4135-9575-037180af010d} nsSessionStore.js +contract @mozilla.org/suite/sessionstore;1 {d37ccdf1-496f-4135-9575-037180af010d} diff --git a/comm/suite/components/sessionstore/nsSessionStore.js b/comm/suite/components/sessionstore/nsSessionStore.js new file mode 100644 index 0000000000..e469af96bb --- /dev/null +++ b/comm/suite/components/sessionstore/nsSessionStore.js @@ -0,0 +1,4174 @@ +/* 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/. */ + +/** + * 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. + */ + +/* :::::::: Constants and Helpers ::::::::::::::: */ + +const STATE_STOPPED = 0; +const STATE_RUNNING = 1; +const STATE_QUITTING = -1; + +const STATE_STOPPED_STR = "stopped"; +const STATE_RUNNING_STR = "running"; + +const TAB_STATE_NEEDS_RESTORE = 1; +const TAB_STATE_RESTORING = 2; + +const PRIVACY_NONE = 0; +const PRIVACY_ENCRYPTED = 1; +const PRIVACY_FULL = 2; + +const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored"; +const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored"; + +// global notifications observed +const OBSERVING = [ + "domwindowclosed", + "quit-application-requested", "quit-application-granted", "quit-application", + "browser-lastwindow-close-granted", "browser:purge-session-history" +]; + +/* +XUL Window properties to (re)store +Restored in restoreDimensions() +*/ +const WINDOW_ATTRIBUTES = { + width: "outerWidth", + height: "outerHeight", + screenX: "screenX", + screenY: "screenY", + sizemode: "windowState" +}; + +/* +Hideable window features to (re)store +Restored in restoreWindowFeatures() +*/ +const WINDOW_HIDEABLE_FEATURES = [ + "menubar", "toolbar", "locationbar", + "personalbar", "statusbar", "scrollbars" +]; + +/* +docShell capabilities to (re)store +Restored in restoreHistory() +eg: browser.docShell["allow" + aCapability] = false; + +XXX keep these in sync with all the attributes starting + with "allow" in /docshell/base/nsIDocShell.idl +*/ +const CAPABILITIES = [ + "Subframes", "Plugins", "Javascript", "MetaRedirects", "Images", + "DNSPrefetch", "Auth", "WindowControl" +]; + +// These are tab events that we listen to. +const TAB_EVENTS = ["TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide"]; + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +var {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "SecMan", + "@mozilla.org/scriptsecuritymanager;1", "nsIScriptSecurityManager"); +XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", + "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"); +XPCOMUtils.defineLazyServiceGetter(this, "uuidGenerator", + "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); + +ChromeUtils.defineModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); +ChromeUtils.defineModuleGetter(this, "Utils", + "resource://gre/modules/sessionstore/Utils.jsm"); +ChromeUtils.defineModuleGetter(this, "XPathGenerator", + "resource:///modules/sessionstore/XPathGenerator.jsm"); + +function debug(aMsg) { + Services.console.logStringMessage("SessionStore: " + aMsg); +} + +/* :::::::: The Service ::::::::::::::: */ + +function SessionStoreService() { + XPCOMUtils.defineLazyGetter(this, "_prefBranch", function () { + return Services.prefs.getBranch("browser."); + }); + + // minimal interval between two save operations (in milliseconds) + XPCOMUtils.defineLazyGetter(this, "_interval", function () { + // used often, so caching/observing instead of fetching on-demand + this._prefBranch.addObserver("sessionstore.interval", this, true); + return this._prefBranch.getIntPref("sessionstore.interval"); + }); + + // when crash recovery is disabled, session data is not written to disk + XPCOMUtils.defineLazyGetter(this, "_resume_from_crash", function () { + // get crash recovery state from prefs and allow for proper reaction to state changes + this._prefBranch.addObserver("sessionstore.resume_from_crash", this, true); + return this._prefBranch.getBoolPref("sessionstore.resume_from_crash"); + }); +} + +SessionStoreService.prototype = { + classID: Components.ID("{d37ccdf1-496f-4135-9575-037180af010d}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore, + Ci.nsIDOMEventListener, + Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + // xul:tab attributes to (re)store (extensions might want to hook in here); + // the favicon is always saved for the about:sessionrestore page + xulAttributes: {"image": true}, + + // set default load state + _loadState: STATE_STOPPED, + + // During the initial restore and setBrowserState calls tracks the number of + // windows yet to be restored + _restoreCount: -1, + + // whether a setBrowserState call is in progress + _browserSetState: false, + + // time in milliseconds (Date.now()) when the session was last written to file + _lastSaveTime: 0, + + // 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: {}, + + // 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, + + // tabs to restore in order + _tabsToRestore: { visible: [], hidden: [] }, + _tabsRestoringCount: 0, + + // number of tabs to restore concurrently, pref controlled. + _maxConcurrentTabRestores: null, + + // 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 + _lastSessionState: null, + + // Whether we've been initialized + _initialized: false, + + // Mapping from legacy docshellIDs to docshellUUIDs. + _docshellUUIDMap: new Map(), + +/* ........ Public Getters .............. */ + + get canRestoreLastSession() { + // Always disallow restoring the previous session when in private browsing + return this._lastSessionState; + }, + + set canRestoreLastSession(val) { + // Cheat a bit; only allow false. + if (!val) + this._lastSessionState = null; + }, + +/* ........ Global Event Handlers .............. */ + + /** + * Initialize the component + */ + initService: function() { + OBSERVING.forEach(function(aTopic) { + Services.obs.addObserver(this, aTopic, true); + }, this); + + this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); + this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true); + + this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); + this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true); + + // this pref is only read at startup, so no need to observe it + this._sessionhistory_max_entries = + this._prefBranch.getIntPref("sessionhistory.max_entries"); + + this._maxConcurrentTabRestores = + this._prefBranch.getIntPref("sessionstore.max_concurrent_tabs"); + this._prefBranch.addObserver("sessionstore.max_concurrent_tabs", this, true); + + // Make sure gRestoreTabsProgressListener has a reference to sessionstore + // so that it can make calls back in + gRestoreTabsProgressListener.ss = this; + + // get file references + this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + this._sessionFileBackup = this._sessionFile.clone(); + this._sessionFile.append("sessionstore.json"); + this._sessionFileBackup.append("sessionstore.bak"); + + // get string containing session state + var ss = Cc["@mozilla.org/suite/sessionstartup;1"] + .getService(Ci.nsISessionStartup); + try { + if (ss.sessionType != Ci.nsISessionStartup.NO_SESSION) + this._initialState = ss.state; + } + catch(ex) { dump(ex + "\n"); } // no state to restore, which is ok + + if (this._initialState) { + 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(this._initialState); + // If we have a iniState with windows, that means that we have windows + // with app tabs to restore. + if (iniState.windows.length) + this._initialState = iniState; + else + this._initialState = null; + if (remainingState.windows.length) + this._lastSessionState = remainingState; + } + else { + // Get the last deferred session in case the user still wants to + // restore it + this._lastSessionState = this._initialState.lastSessionState; + + let lastSessionCrashed = + this._initialState.session && this._initialState.session.state && + this._initialState.session.state == STATE_RUNNING_STR; + if (lastSessionCrashed) { + this._recentCrashes = (this._initialState.session && + this._initialState.session.recentCrashes || 0) + 1; + + if (this._needsRestorePage(this._initialState, this._recentCrashes)) { + // replace the crashed session with a restore-page-only session + let pageData = { + url: "about:sessionrestore", + triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL, + formdata: { "#sessionData": JSON.stringify(this._initialState) } + }; + this._initialState = { windows: [{ tabs: [{ entries: [pageData] }] }] }; + } + } + + // Load the session start time from the previous state + this._sessionStartTime = this._initialState.session && + this._initialState.session.startTime || + this._sessionStartTime; + + // make sure that at least the first window doesn't have anything hidden + delete this._initialState.windows[0].hidden; + // Since nothing is hidden in the first window, it cannot be a popup + delete this._initialState.windows[0].isPopup; + // clear any lastSessionWindowID attributes since those don't matter + // during normal restore + this._initialState.windows.forEach(function(aWindow) { + delete aWindow.__lastSessionWindowID; + }); + } + } + catch (ex) { debug("The session file is invalid: " + ex); } + } + + if (this._resume_from_crash) { + // create a backup if the session data file exists + try { + if (this._sessionFileBackup.exists()) + this._sessionFileBackup.remove(false); + if (this._sessionFile.exists()) + this._sessionFile.copyTo(null, this._sessionFileBackup.leafName); + } + catch (ex) { Cu.reportError(ex); } // file was write-locked? + } + + // 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 (this._loadState != STATE_QUITTING && + this._prefBranch.getBoolPref("sessionstore.resume_session_once")) + this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); + + this._initialized = true; + }, + + /** + * Start tracking a window. + * This function also initializes the component if it's not already + * initialized. + */ + init: function sss_init(aWindow) { + // Initialize the service if needed. + if (!this._initialized) + this.initService(); + + if (aWindow) { + this.onLoad(aWindow); + } else if (this._loadState == STATE_STOPPED) { + // If init is being called with a null window, it's possible that we + // just want to tell sessionstore that a session is live (as is the case + // with starting Firefox with -private, for example; see bug 568816), + // so we should mark the load state as running to make sure that + // things like setBrowserState calls will succeed in restoring the session. + this._loadState = STATE_RUNNING; + } + }, + /** + * Called on application shutdown, after notifications: + * quit-application-granted, quit-application + */ + _uninit: function sss_uninit() { + // save all data for session resuming + this.saveState(true); + + // clear out _tabsToRestore in case it's still holding refs + this._tabsToRestore.visible = null; + this._tabsToRestore.hidden = null; + + // remove the ref to us from the progress listener + gRestoreTabsProgressListener.ss = null; + + // Make sure to break our cycle with the save timer + if (this._saveTimer) { + this._saveTimer.cancel(); + this._saveTimer = null; + } + }, + + /** + * Handle notifications + */ + observe: function sss_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "domwindowclosed": // catch closed windows + this.onClose(aSubject); + break; + case "quit-application-requested": + // get a current snapshot of all windows + this._forEachBrowserWindow(function(aWindow) { + this._collectWindowData(aWindow); + }); + DirtyWindows.clear(); + break; + case "quit-application-granted": + // freeze the data at what we've got (ignoring closing windows) + this._loadState = STATE_QUITTING; + break; + case "browser-lastwindow-close-granted": + // last browser window is quitting. + // remember to restore the last window when another browser window is openend + // 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; + break; + case "quit-application": + if (aData == "restart" && !this._isSwitchingProfile()) { + 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 + this._lastSessionState = null; + } + + this._loadState = STATE_QUITTING; // just to be sure + this._uninit(); + break; + case "browser:purge-session-history": // catch sanitization + this._clearDisk(); + // 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 (this._loadState == STATE_QUITTING) + return; + this._lastSessionState = null; + let openWindows = {}; + this._forEachBrowserWindow(function(aWindow) { + //Hide "Restore Last Session" menu item + let restoreItem = aWindow.document.getElementById("historyRestoreLastSession"); + restoreItem.setAttribute("disabled", "true"); + + Array.from(aWindow.getBrowser().tabs).forEach(function(aTab) { + delete aTab.linkedBrowser.__SS_data; + delete aTab.linkedBrowser.__SS_formDataSaved; + if (aTab.linkedBrowser.__SS_restoreState) + this._resetTabRestoringState(aTab); + }); + openWindows[aWindow.__SSi] = 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 + if (this._getMostRecentBrowserWindow()) + Services.tm.mainThread.dispatch(this.saveState.bind(this, true), + Ci.nsIThread.DISPATCH_NORMAL); + else if (this._loadState == STATE_RUNNING) + this.saveState(true); + break; + case "nsPref:changed": // catch pref changes + 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; + case "sessionstore.interval": + this._interval = this._prefBranch.getIntPref("sessionstore.interval"); + // reset timer and save + if (this._saveTimer) { + this._saveTimer.cancel(); + this._saveTimer = null; + } + this.saveStateDelayed(null, -1); + break; + case "sessionstore.resume_from_crash": + this._resume_from_crash = this._prefBranch.getBoolPref("sessionstore.resume_from_crash"); + // either create the file with crash recovery information or remove it + // (when _loadState is not STATE_RUNNING, that file is used for session resuming instead) + if (this._resume_from_crash) + this.saveState(true); + else if (this._loadState == STATE_RUNNING) + this._clearDisk(); + break; + case "sessionstore.max_concurrent_tabs": + this._maxConcurrentTabRestores = + this._prefBranch.getIntPref("sessionstore.max_concurrent_tabs"); + break; + } + break; + case "timer-callback": // timer call back for delayed saving + this._saveTimer = null; + this.saveState(); + break; + } + }, + +/* ........ Window Event Handlers .............. */ + + /** + * Implement nsIDOMEventListener for handling various window and tab events + */ + handleEvent: function sss_handleEvent(aEvent) { + var win = aEvent.currentTarget.ownerDocument.defaultView; + switch (aEvent.type) { + case "load": + // If __SS_restore_data is set, then we need to restore the document + // (form data, scrolling, etc.). This will only happen when a tab is + // first restored. + if (aEvent.currentTarget.__SS_restore_data) + this.restoreDocument(win, aEvent.currentTarget, aEvent); + // We still need to call onTabLoad, so fall through to "pageshow" case. + case "pageshow": + this.onTabLoad(win, aEvent.currentTarget, aEvent); + break; + case "input": + case "DOMAutoComplete": + this.onTabInput(win, aEvent.currentTarget); + break; + case "TabOpen": + this.onTabAdd(win, aEvent.originalTarget); + break; + case "TabClose": + // aEvent.detail determines if the tab was closed by moving to a different window + if (!aEvent.detail) + this.onTabClose(win, aEvent.originalTarget); + this.onTabRemove(win, aEvent.originalTarget); + break; + case "TabSelect": + this.onTabSelect(win); + break; + case "TabShow": + this.onTabShow(aEvent.originalTarget); + break; + case "TabHide": + this.onTabHide(aEvent.originalTarget); + break; + } + }, + + /** + * If it's the first window load since app start... + * - determine if we're reloading after a crash or a forced-restart + * - restore window state + * - restart downloads + * Set up event listeners for this window's tabs + * @param aWindow + * Window reference + */ + onLoad: function sss_onLoad(aWindow) { + // return if window has already been initialized + if (aWindow && aWindow.__SSi && this._windows[aWindow.__SSi]) + return; + + // ignore non-browser windows and windows opened while shutting down + if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || + this._loadState == STATE_QUITTING) + return; + + // assign it a unique identifier (timestamp) + aWindow.__SSi = "window" + Date.now(); + + // and create its data object + this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [] }; + + if (!this._isWindowLoaded(aWindow)) + this._windows[aWindow.__SSi]._restoring = true; + if (!aWindow.toolbar.visible) + this._windows[aWindow.__SSi].isPopup = true; + + // perform additional initialization when the first window is loading + if (this._loadState == STATE_STOPPED) { + this._loadState = STATE_RUNNING; + this._lastSaveTime = Date.now(); + + // restore a crashed session resp. resume the last session if requested + if (this._initialState) { + // make sure that the restored tabs are first in the window + this._initialState._firstTabs = true; + this._restoreCount = this._initialState.windows ? this._initialState.windows.length : 0; + this.restoreWindow(aWindow, this._initialState, + this._isCmdLineEmpty(aWindow)); + delete this._initialState; + + // _loadState changed from "stopped" to "running" + // force a save operation so that crashes happening during startup are correctly counted + this.saveState(true); + } + else { + // Nothing to restore, notify observers things are complete. + this.windowToFocus = aWindow; + Services.tm.mainThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL); + + // the next delayed save request should execute immediately + this._lastSaveTime -= this._interval; + } + } + // this window was opened by _openWindowWithState + else if (!this._isWindowLoaded(aWindow)) { + let followUp = this._statesToRestore[aWindow.__SS_restoreID].windows.length == 1; + this.restoreWindow(aWindow, this._statesToRestore[aWindow.__SS_restoreID], true, followUp); + } + else if (this._restoreLastWindow && aWindow.toolbar.visible && + this._closedWindows.length) { + // 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] }; + this.restoreWindow(aWindow, state, this._isCmdLineEmpty(aWindow)); + } + } + // 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; + } + + var tabbrowser = aWindow.getBrowser(); + + // add tab change listeners to all already existing tabs + for (let i = 0; i < tabbrowser.tabs.length; i++) { + this.onTabAdd(aWindow, tabbrowser.tabs[i], true); + } + // notification of tab add/remove/selection/show/hide + TAB_EVENTS.forEach(function(aEvent) { + tabbrowser.tabContainer.addEventListener(aEvent, this, true); + }, this); + }, + + /** + * On window close... + * - remove event listeners from tabs + * - save all window data + * @param aWindow + * Window reference + */ + onClose: function sss_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 = "window" + Date.now(); + 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; + } + + if (this.windowToFocus && this.windowToFocus == aWindow) { + delete this.windowToFocus; + } + + var tabbrowser = aWindow.getBrowser(); + + TAB_EVENTS.forEach(function(aEvent) { + tabbrowser.tabContainer.removeEventListener(aEvent, this, true); + }, this); + + // remove the progress listener for this window + try { + tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener); + } catch (ex) {}; + + let winData = this._windows[aWindow.__SSi]; + if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down + // update all window data for a last time + this._collectWindowData(aWindow); + + if (isFullyLoaded) { + winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label; + winData.title = this._replaceLoadingTitle(winData.title, tabbrowser, + tabbrowser.selectedTab); + this._updateCookies([winData]); + } + + // save the window if it has multiple tabs or a single saveable tab + if (winData.tabs.length > 1 || + (winData.tabs.length == 1 && this._shouldSaveTabState(winData.tabs[0]))) { + this._closedWindows.unshift(winData); + this._capClosedWindows(); + } + + // clear this window from the list + delete this._windows[aWindow.__SSi]; + + // save the state without this window to disk + this.saveStateDelayed(); + } + + for (let i = 0; i < tabbrowser.tabs.length; i++) { + this.onTabRemove(aWindow, tabbrowser.tabs[i], true); + } + + // Cache the window state until it is completely gone. + DyingWindowCache.set(aWindow, winData); + + delete aWindow.__SSi; + }, + + /** + * set up listeners for a new tab + * @param aWindow + * Window reference + * @param aTab + * Tab reference + * @param aNoNotification + * bool Do not save state if we're updating an existing tab + */ + onTabAdd: function sss_onTabAdd(aWindow, aTab, aNoNotification) { + let browser = aTab.linkedBrowser; + browser.addEventListener("load", this, true); + browser.addEventListener("pageshow", this, true); + browser.addEventListener("input", this, true); + browser.addEventListener("DOMAutoComplete", this, true); + + if (!aNoNotification) { + this.saveStateDelayed(aWindow); + } + + this._updateCrashReportURL(aWindow); + }, + + /** + * 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 sss_onTabRemove(aWindow, aTab, aNoNotification) { + let browser = aTab.linkedBrowser; + browser.removeEventListener("load", this, true); + browser.removeEventListener("pageshow", this, true); + browser.removeEventListener("change", this, true); + browser.removeEventListener("input", this, true); + browser.removeEventListener("DOMAutoComplete", this, true); + + delete browser.__SS_data; + + // 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 sss_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; + } + + // make sure that the tab related data is up-to-date + var tabState = this._collectTabData(aTab); + this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState); + + // store closed-tab data for undo + if (this._shouldSaveTabState(tabState)) { + aTab.tabData = { state: tabState }; + var closedTabs = this._windows[aWindow.__SSi]._closedTabs; + closedTabs.unshift(aTab.tabData); + if (closedTabs.length > this._max_tabs_undo) + closedTabs.length = this._max_tabs_undo; + }; + }, + + /** + * When a tab loads, save state. + * @param aWindow + * Window reference + * @param aBrowser + * Browser reference + * @param aEvent + * Event obj + */ + onTabLoad: function sss_onTabLoad(aWindow, aBrowser, aEvent) { + // react on "load" and solitary "pageshow" events (the first "pageshow" + // following "load" is too late for deleting the data caches) + // It's possible to get a load event after calling stop on a browser (when + // overwriting tabs). We want to return early if the tab hasn't been restored yet. + if ((aEvent.type != "load" && !aEvent.persisted) || + (aBrowser.__SS_restoreState && + aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)) { + return; + } + + delete aBrowser.__SS_data; + this.saveStateDelayed(aWindow); + + // attempt to update the current URL we send in a crash report + this._updateCrashReportURL(aWindow); + }, + + /** + * Called when a browser sends the "input" notification + * @param aWindow + * Window reference + * @param aBrowser + * Browser reference + */ + onTabInput: function sss_onTabInput(aWindow, aBrowser) { + this.saveStateDelayed(aWindow, 3000); + }, + + /** + * When a tab is selected, save session data + * @param aWindow + * Window reference + */ + onTabSelect: function sss_onTabSelect(aWindow) { + if (this._loadState == STATE_RUNNING) { + this._windows[aWindow.__SSi].selected = aWindow.getBrowser().tabContainer.selectedIndex; + + let tab = aWindow.getBrowser().selectedTab; + // If __SS_restoreState is still on the browser and it is + // TAB_STATE_NEEDS_RESTORE, then then we haven't restored + // this tab yet. Explicitly call restoreTab to kick off the restore. + if (tab.linkedBrowser.__SS_restoreState && + tab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) + this.restoreTab(tab); + + // attempt to update the current URL we send in a crash report + this._updateCrashReportURL(aWindow); + } + }, + + onTabShow: function sss_onTabShow(aTab) { + // If the tab hasn't been restored yet, move it into the right _tabsToRestore bucket + if (aTab.linkedBrowser.__SS_restoreState && + aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + this._tabsToRestore.hidden.splice(this._tabsToRestore.hidden.indexOf(aTab), 1); + // Just put it at the end of the list of visible tabs; + this._tabsToRestore.visible.push(aTab); + } + }, + + onTabHide: function sss_onTabHide(aTab) { + // If the tab hasn't been restored yet, move it into the right _tabsToRestore bucket + if (aTab.linkedBrowser.__SS_restoreState && + aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + this._tabsToRestore.visible.splice(this._tabsToRestore.visible.indexOf(aTab), 1); + // Just put it at the end of the list of hidden tabs; + this._tabsToRestore.hidden.push(aTab); + } + }, + +/* ........ nsISessionStore API .............. */ + + getBrowserState: function sss_getBrowserState() { + return this._toJSONString(this._getCurrentState()); + }, + + setBrowserState: function sss_setBrowserState(aState) { + this._handleClosedWindows(); + + try { + var state = JSON.parse(aState); + } + catch (ex) { /* invalid state object - don't restore anything */ } + if (!state || !state.windows) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + this._browserSetState = true; + + // Make sure _tabsToRestore 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; + + // restore to the given state + this.restoreWindow(window, state, true); + }, + + getWindowState: function sss_getWindowState(aWindow) { + if ("__SSi" in aWindow) { + return this._toJSONString(this._getWindowState(aWindow)); + } + + if (DyingWindowCache.has(aWindow)) { + let data = DyingWindowCache.get(aWindow); + return this._toJSONString({ windows: [data] }); + } + + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + setWindowState: function sss_setWindowState(aWindow, aState, aOverwrite) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + this.restoreWindow(aWindow, aState, aOverwrite); + }, + + getTabState: function sss_getTabState(aTab) { + if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var tabState = this._collectTabData(aTab); + + var window = aTab.ownerDocument.defaultView; + this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState); + + return this._toJSONString(tabState); + }, + + setTabState: function sss_setTabState(aTab, aState) { + var tabState = JSON.parse(aState); + if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var window = aTab.ownerDocument.defaultView; + this._sendWindowStateEvent(window, "Busy"); + this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0); + }, + + duplicateTab: function sss_duplicateTab(aWindow, aTab, aDelta, aRelated) { + if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi || + aWindow && !aWindow.getBrowser) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var tabState = this._collectTabData(aTab, true); + var sourceWindow = aTab.ownerDocument.defaultView; + this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true); + tabState.index += aDelta; + tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); + + if (aWindow) { + this._sendWindowStateEvent(aWindow, "Busy"); + var newTab = aWindow.getBrowser() + .addTab(null, { relatedToCurrent: aRelated }); + this.restoreHistoryPrecursor(aWindow, [newTab], [tabState], 0, 0, 0); + return newTab; + } + + var state = { windows: [{ tabs: [tabState] }] }; + this.windowToFocus = this._openWindowWithState(state); + return null; + }, + + _getClosedTabs: function sss_getClosedTabs(aWindow) { + if (!aWindow.__SSi) + return this._toJSONString(aWindow.__SS_dyingCache._closedTabs); + + var closedTabs = this._windows[aWindow.__SSi]._closedTabs; + closedTabs = closedTabs.concat(aWindow.getBrowser().savedBrowsers); + closedTabs = closedTabs.filter(function(aTabData, aIndex, aArray) { + return aArray.indexOf(aTabData) == aIndex; + }); + return closedTabs; + }, + + getClosedTabCount: function sss_getClosedTabCount(aWindow) { + if ("__SSi" in aWindow) { + return this._windows[aWindow.__SSi]._closedTabs.length; + } + + if (DyingWindowCache.has(aWindow)) { + return DyingWindowCache.get(aWindow)._closedTabs.length; + } + + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + getClosedTabData: function sss_getClosedTabData(aWindow) { + if ("__SSi" in aWindow) { + return this._toJSONString(this._windows[aWindow.__SSi]._closedTabs); + } + + if (DyingWindowCache.has(aWindow)) { + let data = DyingWindowCache.get(aWindow); + return this._toJSONString(data._closedTabs); + } + + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + undoCloseTab: function sss_undoCloseTab(aWindow, aIndex) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var closedTabs = this._getClosedTabs(aWindow); + if (!(aIndex in closedTabs)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // fetch the data of closed tab, while removing it from the array + let closedTab = closedTabs[aIndex]; + if (aIndex in this._windows[aWindow.__SSi]._closedTabs) + this._windows[aWindow.__SSi]._closedTabs.splice(aIndex, 1); + var tabbrowser = aWindow.getBrowser(); + var index = tabbrowser.savedBrowsers.indexOf(closedTab); + this._sendWindowStateEvent(aWindow, "Busy"); + if (index != -1) + // SeaMonkey has its own undoclosetab functionality + return tabbrowser.restoreTab(index); + + // create a new tab + var tab = tabbrowser.addTab(); + + // restore the tab's position + tabbrowser.moveTabTo(tab, closedTab.pos); + + // restore tab content + this.restoreHistoryPrecursor(aWindow, [tab], [closedTab.state], 1, 0, 0); + + // focus the tab's content area (bug 342432) + tab.linkedBrowser.focus(); + + return tab; + }, + + forgetClosedTab: function sss_forgetClosedTab(aWindow, aIndex) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var closedTabs = this._getClosedTabs(aWindow); + if (!(aIndex in closedTabs)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // remove closed tab from the array + var closedTab = closedTabs[aIndex]; + if (aIndex in this._windows[aWindow.__SSi]._closedTabs) + this._windows[aWindow.__SSi]._closedTabs.splice(aIndex, 1); + var tabbrowser = aWindow.getBrowser(); + var index = tabbrowser.savedBrowsers.indexOf(closedTab); + if (index != -1) + tabbrowser.forgetSavedBrowser(aIndex); + }, + + getClosedWindowCount: function sss_getClosedWindowCount() { + return this._closedWindows.length; + }, + + getClosedWindowData: function sss_getClosedWindowData() { + return this._toJSONString(this._closedWindows); + }, + + undoCloseWindow: function sss_undoCloseWindow(aIndex) { + if (!(aIndex in this._closedWindows)) + return null; + + // reopen the window + let state = { windows: this._closedWindows.splice(aIndex, 1) }; + let window = this._openWindowWithState(state); + this.windowToFocus = window; + return window; + }, + + forgetClosedWindow: function sss_forgetClosedWindow(aIndex) { + // default to the most-recently closed window + aIndex = aIndex || 0; + if (!(aIndex in this._closedWindows)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // remove closed window from the array + this._closedWindows.splice(aIndex, 1); + }, + + getWindowValue: function sss_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.returnCode = Cr.NS_ERROR_INVALID_ARG); + }, + + setWindowValue: function sss_setWindowValue(aWindow, aKey, aStringValue) { + if (aWindow.__SSi) { + if (!this._windows[aWindow.__SSi].extData) { + this._windows[aWindow.__SSi].extData = {}; + } + this._windows[aWindow.__SSi].extData[aKey] = aStringValue; + this.saveStateDelayed(aWindow); + } + else { + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + }, + + deleteWindowValue: function sss_deleteWindowValue(aWindow, aKey) { + if (aWindow.__SSi && this._windows[aWindow.__SSi].extData && + this._windows[aWindow.__SSi].extData[aKey]) + delete this._windows[aWindow.__SSi].extData[aKey]; + }, + + getTabValue: function sss_getTabValue(aTab, aKey) { + let data = {}; + if (aTab.__SS_extdata) { + data = aTab.__SS_extdata; + } + else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { + // If the tab hasn't been fully restored, get the data from the to-be-restored data + data = aTab.linkedBrowser.__SS_data.extData; + } + return data[aKey] || ""; + }, + + setTabValue: function sss_setTabValue(aTab, aKey, aStringValue) { + // If the tab hasn't been restored, then set the data there, otherwise we + // could lose newly added data. + let saveTo; + if (aTab.__SS_extdata) { + saveTo = aTab.__SS_extdata; + } + else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { + saveTo = aTab.linkedBrowser.__SS_data.extData; + } + else { + aTab.__SS_extdata = {}; + saveTo = aTab.__SS_extdata; + } + saveTo[aKey] = aStringValue; + this.saveStateDelayed(aTab.ownerDocument.defaultView); + }, + + deleteTabValue: function sss_deleteTabValue(aTab, aKey) { + // We want to make sure that if data is accessed early, we attempt to delete + // that data from __SS_data as well. Otherwise we'll throw in cases where + // data can be set or read. + let deleteFrom = null; + if (aTab.__SS_extdata) { + deleteFrom = aTab.__SS_extdata; + } + else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { + deleteFrom = aTab.linkedBrowser.__SS_data.extData; + } + + if (deleteFrom && deleteFrom[aKey]) + delete deleteFrom[aKey]; + }, + + persistTabAttribute: function sss_persistTabAttribute(aName) { + if (aName in this.xulAttributes) + return; // this attribute is already being tracked + + this.xulAttributes[aName] = true; + this.saveStateDelayed(); + }, + + doRestoreLastWindow: function sss_doRestoreLastWindow() { + let state = null; + this._closedWindows.forEach(function(aWinState) { + if (!state && !aWinState.isPopup) { + state = aWinState; + } + }); + return (this._restoreLastWindow && state && + this._doResumeSession()); + }, + + /** + * Restores the session state stored in _lastSessionState. 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 winddow. Otherwise new windows will + * be opened. + */ + restoreLastSession: function sss_restoreLastSession() { + // Use the public getter since it also checks PB mode + if (!this.canRestoreLastSession) + throw (Components.returnCode = Cr.NS_ERROR_FAILURE); + + // First collect each window with its id... + let windows = {}; + this._forEachBrowserWindow(function(aWindow) { + if (aWindow.__SS_lastSessionWindowID) + windows[aWindow.__SS_lastSessionWindowID] = aWindow; + }); + + let lastSessionState = this._lastSessionState; + + // This shouldn't ever be the case... + if (!lastSessionState.windows.length) + throw (Components.returnCode = 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; + + // 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. The hack we have + // in _preWindowToRestoreInto will prevent most (all?) Panorama + // weirdness but we will still merge other extData. + // Bug 588217 should make this go away by merging the group data. + this.restoreWindow(windowToUse, { windows: [winState] }, canOverwriteTabs, true); + } + 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(); + } + + // Set data that persists between sessions + this._recentCrashes = lastSessionState.session && + lastSessionState.session.recentCrashes || 0; + this._sessionStartTime = lastSessionState.session && + lastSessionState.session.startTime || + this._sessionStartTime; + + this._lastSessionState = null; + }, + + /** + * 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 sss__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; + + // Step 1 of processing: + // Inspect extData for Panorama identifiers. If found, then we want to + // inspect further. If there is a single group, then we can use this + // window. If there are multiple groups then we won't use this window. + let data = this.getWindowValue(aWindow, "tabview-group"); + if (data) { + data = JSON.parse(data); + + // Multiple keys means multiple groups, which means we don't want to use this window. + if (Object.keys(data).length > 1) { + return [false, false]; + } + else { + // If there is only one group, then we want to ensure that its group id + // is 0. This is how Panorama forces group merging when new tabs are opened. + //XXXzpao This is a hack and the proper fix really belongs in Panorama. + let groupKey = Object.keys(data)[0]; + if (groupKey !== "0") { + data["0"] = data[groupKey]; + delete data[groupKey]; + this.setWindowValue(aWindow, "tabview-groups", JSON.stringify(data)); + } + } + } + + // Step 2 of processing: + // If we're still here, then the window is usable. 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. + let homePages = aWindow.getHomePage(); + let removableTabs = []; + let tabbrowser = aWindow.getBrowser(); + let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs; + for (let i = 0; i < tabbrowser.tabs.length; i++) { + let tab = tabbrowser.tabs[i]; + if (homePages.includes(tab.linkedBrowser.currentURI.spec)) { + 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 all session data for a window + * @param aWindow + * Window reference + */ + _saveWindowHistory: function sss_saveWindowHistory(aWindow) { + var tabbrowser = aWindow.getBrowser(); + var tabs = tabbrowser.tabs; + var tabsData = this._windows[aWindow.__SSi].tabs = []; + + for (var i = 0; i < tabs.length; i++) + tabsData.push(this._collectTabData(tabs[i])); + + this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1; + }, + + /** + * Collect data related to a single tab + * @param aTab + * tabbrowser tab + * @param aFullData + * always return privacy sensitive data (use with care) + * @returns object + */ + _collectTabData: function sss_collectTabData(aTab, aFullData) { + var tabData = { entries: [] }; + var browser = aTab.linkedBrowser; + + if (!browser || !browser.currentURI) + // can happen when calling this function right after .addTab() + return tabData; + else if (browser.__SS_data && + browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + // use the data to be restored when the tab hasn't been completely loaded + tabData = browser.__SS_data; + if (aTab.pinned) + tabData.pinned = true; + else + delete tabData.pinned; + tabData.hidden = aTab.hidden; + + // If __SS_extdata is set then we'll use that since it might be newer. + if (aTab.__SS_extdata) + tabData.extData = aTab.__SS_extdata; + // If it exists but is empty then a key was likely deleted. In that case just + // delete extData. + if (tabData.extData && !Object.keys(tabData.extData).length) + delete tabData.extData; + return tabData; + } + + var history = null; + try { + history = browser.sessionHistory; + } + catch (ex) { } // this could happen if we catch a tab during (de)initialization + + // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse + // data even when we shouldn't (e.g. Back, different anchor) + if (history && browser.__SS_data && + browser.__SS_data.entries[history.index] && + browser.__SS_data.entries[history.index].url == browser.currentURI.spec && + history.index < this._sessionhistory_max_entries - 1 && !aFullData) { + tabData = browser.__SS_data; + tabData.index = history.index + 1; + } + else if (history && history.count > 0) { + try { + for (var j = 0; j < history.count; j++) { + let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j), + aFullData, aTab.pinned); + tabData.entries.push(entry); + } + // If we make it through the for loop, then we're ok and we should clear + // any indicator of brokenness. + delete aTab.__SS_broken_history; + } + catch (ex) { + // In some cases, getEntryAtIndex will throw. This seems to be due to + // history.count being higher than it should be. By doing this in a + // try-catch, we'll update history to where it breaks, assert for + // non-release builds, and still save sessionstore.js. We'll track if + // we've shown the assert for this tab so we only show it once. + // cf. bug 669196. + if (!aTab.__SS_broken_history) { + // First Focus the window & tab we're having trouble with. + aTab.ownerDocument.defaultView.focus(); + aTab.ownerDocument.defaultView.getBrowser().selectedTab = aTab; + debug("SessionStore failed gathering complete history " + + "for the focused window/tab. See bug 669196."); + aTab.__SS_broken_history = true; + } + } + tabData.index = history.index + 1; + + // make sure not to cache privacy sensitive data which shouldn't get out + if (!aFullData) + browser.__SS_data = tabData; + } + else if (browser.currentURI.spec != "about:blank" || + browser.contentDocument.body.hasChildNodes()) { + tabData.entries[0] = { url: browser.currentURI.spec, + triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL }; + tabData.index = 1; + } + + // 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. userTypedClear + // is used to indicate whether the tab was in some sort of loading state with + // userTypedValue. + if (browser.userTypedValue) { + tabData.userTypedValue = browser.userTypedValue; + tabData.userTypedClear = browser.userTypedClear; + } else { + delete tabData.userTypedValue; + delete tabData.userTypedClear; + } + + var disallow = []; + for (var i = 0; i < CAPABILITIES.length; i++) + if (!browser.docShell["allow" + CAPABILITIES[i]]) + disallow.push(CAPABILITIES[i]); + if (disallow.length > 0) + tabData.disallow = disallow.join(","); + else if (tabData.disallow) + delete tabData.disallow; + + tabData.attributes = {}; + for (let name in this.xulAttributes) { + if (aTab.hasAttribute(name)) + tabData.attributes[name] = aTab.getAttribute(name); + } + + if (aTab.__SS_extdata) + tabData.extData = aTab.__SS_extdata; + else if (tabData.extData) + delete tabData.extData; + + if (history && browser.docShell instanceof Ci.nsIDocShell) + this._serializeSessionStorage(tabData, history, browser.docShell, aFullData, + false); + + return tabData; + }, + + /** + * Get an object that is a serialized representation of a History entry + * Used for data storage + * @param aEntry + * nsISHEntry instance + * @param aFullData + * always return privacy sensitive data (use with care) + * @param aIsPinned + * the tab is pinned and should be treated differently for privacy + * @returns object + */ + _serializeHistoryEntry: + function sss_serializeHistoryEntry(aEntry, aFullData, aIsPinned) { + var entry = { url: aEntry.URI.spec, + triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL }; + + if (aEntry.title && aEntry.title != entry.url) { + entry.title = aEntry.title; + } + if (aEntry.isSubFrame) { + entry.subframe = true; + } + if (!(aEntry instanceof Ci.nsISHEntry)) { + return entry; + } + + var cacheKey = aEntry.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 = aEntry.ID; + entry.docshellUUID = aEntry.docshellID.toString(); + + if (aEntry.referrerURI) + entry.referrer = aEntry.referrerURI.spec; + + if (aEntry.contentType) + entry.contentType = aEntry.contentType; + + var x = {}, y = {}; + aEntry.getScrollPosition(x, y); + if (x.value != 0 || y.value != 0) + entry.scroll = x.value + "," + y.value; + + try { + var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata"); + if (aEntry.postData && (aFullData || prefPostdata && + this._checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) { + aEntry.postData.QueryInterface(Ci.nsISeekableStream) + .seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + var stream = Cc["@mozilla.org/binaryinputstream;1"] + .createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(aEntry.postData); + var postBytes = stream.readByteArray(stream.available()); + var postdata = String.fromCharCode.apply(null, postBytes); + if (aFullData || prefPostdata == -1 || + postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <= + prefPostdata) { + // We can stop doing base64 encoding once our serialization into JSON + // is guaranteed to handle all chars in strings, including embedded + // nulls. + entry.postdata_b64 = btoa(postdata); + } + } + } + catch (ex) { debug(ex); } // POSTDATA is tricky - especially since some extensions don't get it right + + // 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 Gecko 55) + // we can update this code, remove triggeringPrincipal_b64 and + // just keep triggeringPrincipal_base64 as well as + // principalToInherit_base64. + if (aEntry.principalToInherit) { + try { + let principalToInherit = Utils.serializePrincipal(aEntry.principalToInherit); + if (principalToInherit) { + entry.triggeringPrincipal_b64 = principalToInherit; + entry.principalToInherit_base64 = principalToInherit; + } + } catch (e) { + debug(e); + } + } + + if (aEntry.triggeringPrincipal) { + try { + let triggeringPrincipal = Utils.serializePrincipal(aEntry.triggeringPrincipal); + if (triggeringPrincipal) { + entry.triggeringPrincipal_base64 = triggeringPrincipal; + } + } catch (e) { + debug(e); + } + } + + entry.docIdentifier = aEntry.BFCacheEntry.ID; + + if (aEntry.stateData) { + entry.structuredCloneState = aEntry.stateData.getDataAsBase64(); + entry.structuredCloneVersion = aEntry.stateData.formatVersion; + } + + if (!(aEntry instanceof Ci.nsISHContainer)) { + return entry; + } + + if (aEntry.childCount > 0) { + entry.children = []; + for (var i = 0; i < aEntry.childCount; i++) { + var child = aEntry.GetChildAt(i); + if (child) { + entry.children.push(this._serializeHistoryEntry(child, aFullData, aIsPinned)); + } + else { // to maintain the correct frame order, insert a dummy entry + entry.children.push({ url: "about:blank", + triggeringPrincipal_base64: Utils.SERIALIZED_SYSTEMPRINCIPAL}); + } + // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) + if (/^wyciwyg:\/\//.test(entry.children[i].url)) { + delete entry.children; + break; + } + } + } + + return entry; + }, + + /** + * Updates all sessionStorage "super cookies" + * @param aTabData + * The data object for a specific tab + * @param aHistory + * That tab's session history + * @param aDocShell + * That tab's docshell (containing the sessionStorage) + * @param aFullData + * always return privacy sensitive data (use with care) + * @param aIsPinned + * the tab is pinned and should be treated differently for privacy + */ + _serializeSessionStorage: + function sss_serializeSessionStorage(aTabData, aHistory, aDocShell, aFullData, aIsPinned) { + let storageData = {}; + let hasContent = false; + + for (let i = 0; i < aHistory.count; i++) { + let principal; + try { + let uri = aHistory.getEntryAtIndex(i).URI; + principal = SecMan.getDocShellCodebasePrincipal(uri, aDocShell); + } + catch (ex) { + // Chances are that this is getEntryAtIndex throwing, as seen in bug 669196. + // We've already asserted in _collectTabData, so we won't show that again. + continue; + } + + // sessionStorage is saved per principal (cf. nsGlobalWindow::GetSessionStorage) + let origin; + try { + origin = principal.origin; + } + catch (ex) { + origin = principal.URI.spec; + } + + if (storageData[origin]) + continue; + + let isHTTPS = principal.URI && principal.URI.schemeIs("https"); + if (!(aFullData || this._checkPrivacyLevel(isHTTPS, aIsPinned))) + continue; + + let storage, storageItemCount = 0; + + let window = aDocShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + try { + let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager); + storage = storageManager.getStorage(window, principal); + + // See Bug 1232955 - storage.length can throw, catch that failure here inside the try. + if (storage) + storageItemCount = storage.length; + } + catch (ex) { /* sessionStorage might throw if it's turned off, see bug 458954 */ } + + if (storageItemCount == 0) + continue; + + let data = storageData[origin] = {}; + + for (let j = 0; j < storageItemCount; j++) { + try { + let key = storage.key(j); + data[key] = storage.getItem(key); + } + catch (ex) { /* XXXzeniko this currently throws for secured items (cf. bug 442048) */ } + } + hasContent = true; + } + + if (hasContent) + aTabData.storage = storageData; + }, + + /** + * go through all tabs and store the current scroll positions + * and innerHTML content of WYSIWYG editors + * @param aWindow + * Window reference + */ + _updateTextAndScrollData: function sss_updateTextAndScrollData(aWindow) { + var browsers = aWindow.getBrowser().browsers; + for (var i = 0; i < browsers.length; i++) { + try { + var tabData = this._windows[aWindow.__SSi].tabs[i]; + if (browsers[i].__SS_data && + browsers[i].__SS_restoreState == TAB_STATE_NEEDS_RESTORE) + continue; // ignore incompletely initialized tabs + this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData); + } + catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time) + } + }, + + /** + * go through all frames and store the current scroll positions + * and innerHTML content of WYSIWYG editors + * @param aWindow + * Window reference + * @param aBrowser + * single browser reference + * @param aTabData + * tabData object to add the information to + * @param aFullData + * always return privacy sensitive data (use with care) + */ + _updateTextAndScrollDataForTab: + function sss_updateTextAndScrollDataForTab(aWindow, aBrowser, aTabData, aFullData) { + var tabIndex = (aTabData.index || aTabData.entries.length) - 1; + // entry data needn't exist for tabs just initialized with an incomplete session state + if (!aTabData.entries[tabIndex]) + return; + + let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : + this._getSelectedPageStyle(aBrowser.contentWindow); + if (selectedPageStyle) + aTabData.pageStyle = selectedPageStyle; + else if (aTabData.pageStyle) + delete aTabData.pageStyle; + + this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow, + aTabData.entries[tabIndex], + aFullData, + !!aTabData.pinned); + if (aBrowser.currentURI.spec == "about:config") + aTabData.entries[tabIndex].formdata = { + "#textbox": aBrowser.contentDocument.getElementById("textbox").value + }; + }, + + /** + * go through all subframes and store all form data, the current + * scroll positions and innerHTML content of WYSIWYG editors + * @param aWindow + * Window reference + * @param aContent + * frame reference + * @param aData + * part of a tabData object to add the information to + * @param aFullData + * always return privacy sensitive data (use with care) + * @param aIsPinned + * the tab is pinned and should be treated differently for privacy + */ + _updateTextAndScrollDataForFrame: + function sss_updateTextAndScrollDataForFrame(aWindow, aContent, aData, + aFullData, aIsPinned) { + for (var i = 0; i < aContent.frames.length; i++) { + if (aData.children && aData.children[i]) + this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i], + aData.children[i], + aFullData, aIsPinned); + } + var isHTTPS = this._getURIFromString((aContent.parent || aContent). + document.location.href).schemeIs("https"); + if (aFullData || this._checkPrivacyLevel(isHTTPS, aIsPinned) || + aContent.top.document.location.href == "about:sessionrestore") { + let formData = this._collectFormDataForFrame(aContent.document); + if (formData) + aData.formdata = formData; + else if (aData.formdata) + delete aData.formdata; + + // designMode is undefined e.g. for XUL documents (as about:config) + if ((aContent.document.designMode || "") == "on") { + if (aData.innerHTML === undefined && !aFullData) { + // we get no "input" events from iframes - listen for keypress here + aContent.addEventListener("keypress", this.saveStateDelayed.bind(this, aWindow, 3000), true); + } + aData.innerHTML = aContent.document.body.innerHTML; + } + } + + // get scroll position from nsIDOMWindowUtils, since it allows avoiding a + // flush of layout + let domWindowUtils = aContent.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let scrollX = {}, scrollY = {}; + domWindowUtils.getScrollXY(false, scrollX, scrollY); + aData.scroll = scrollX.value + "," + scrollY.value; + }, + + /** + * determine the title of the currently enabled style sheet (if any) + * and recurse through the frameset if necessary + * @param aContent is a frame reference + * @returns the title style sheet determined to be enabled (empty string if none) + */ + _getSelectedPageStyle: function sss_getSelectedPageStyle(aContent) { + const forScreen = /(?:^|,)\s*(?:all|screen)\s*(?:,|$)/i; + for (let i = 0; i < aContent.document.styleSheets.length; i++) { + let ss = aContent.document.styleSheets[i]; + let media = ss.media.mediaText; + if (!ss.disabled && ss.title && (!media || forScreen.test(media))) + return ss.title + } + for (let i = 0; i < aContent.frames.length; i++) { + let selectedPageStyle = this._getSelectedPageStyle(aContent.frames[i]); + if (selectedPageStyle) + return selectedPageStyle; + } + return ""; + }, + + /** + * collect the state of all form elements + * @param aDocument + * document reference + */ + _collectFormDataForFrame: function sss_collectFormDataForFrame(aDocument) { + let formNodes = aDocument.evaluate(XPathGenerator.restorableFormNodes, aDocument, + XPathGenerator.resolveNS, + aDocument.defaultView.XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); + let node = formNodes.iterateNext(); + if (!node) + return null; + + const MAX_GENERATED_XPATHS = 100; + let generatedCount = 0; + + let data = {}; + do { + let nId = node.id; + let hasDefaultValue = true; + let value; + + // Only generate a limited number of XPath expressions for perf reasons (cf. bug 477564) + if (!nId && generatedCount > MAX_GENERATED_XPATHS) + continue; + + if (ChromeUtils.getClassName(node) === "HTMLInputElement" || + ChromeUtils.getClassName(node) === "HTMLTextAreaElement") { + switch (node.type) { + case "checkbox": + case "radio": + value = node.checked; + hasDefaultValue = value == node.defaultChecked; + break; + case "file": + value = { type: "file", fileList: node.mozGetFileNameArray() }; + hasDefaultValue = !value.fileList.length; + break; + default: // text, textarea + value = node.value; + hasDefaultValue = value == node.defaultValue; + break; + } + } + else if (!node.multiple) { + // <select>s without the multiple attribute are hard to determine the + // default value, so assume we don't have the default. + hasDefaultValue = false; + value = node.selectedIndex; + } + else { + // <select>s with the multiple attribute are easier to determine the + // default value since each <option> has a defaultSelected + let options = Array.from(node.options, function(aOpt, aIx) { + let oSelected = aOpt.selected; + hasDefaultValue = hasDefaultValue && (oSelected == aOpt.defaultSelected); + return oSelected ? aIx : -1; + }); + value = options.filter(aIx => aIx >= 0); + } + // In order to reduce XPath generation (which is slow), we only save data + // for form fields that have been changed. (cf. bug 537289) + if (!hasDefaultValue) { + if (nId) { + data["#" + nId] = value; + } + else { + generatedCount++; + data[XPathGenerator.generate(node)] = value; + } + } + + } while ((node = formNodes.iterateNext())); + + return data; + }, + + /** + * extract the base domain from a history entry and its children + * @param aEntry + * the history entry, serialized + * @param aHosts + * the hash that will be used to store hosts eg, { hostname: true } + * @param aCheckPrivacy + * should we check the privacy level for https + * @param aIsPinned + * is the entry we're evaluating for a pinned tab; used only if + * aCheckPrivacy + */ + _extractHostsForCookiesFromEntry: + function sss__extractHostsForCookiesFromEntry(aEntry, aHosts, aCheckPrivacy, aIsPinned) { + + if (aEntry.children) { + aEntry.children.forEach(function(entry) { + this._extractHostsForCookiesFromEntry(entry, aHosts, aCheckPrivacy, aIsPinned); + }, this); + } + }, + + /** + * extract the base domain from a host & scheme + * @param aHost + * the host of a uri (usually via nsIURI.host) + * @param aScheme + * the scheme of a uri (usually via nsIURI.scheme) + * @param aHosts + * the hash that will be used to store hosts eg, { hostname: true } + * @param aCheckPrivacy + * should we check the privacy level for https + * @param aIsPinned + * is the entry we're evaluating for a pinned tab; used only if + * aCheckPrivacy + */ + _extractHostsForCookiesFromHostScheme: + function sss__extractHostsForCookiesFromHostScheme(aHost, aScheme, aHosts, aCheckPrivacy, aIsPinned) { + // host and scheme may not be set (for about: urls for example), in which + // case testing scheme will be sufficient. + if (/https?/.test(aScheme) && !aHosts[aHost] && + (!aCheckPrivacy || + this._checkPrivacyLevel(aScheme == "https", aIsPinned))) { + // By setting this to true or false, we can determine when looking at + // the host in _updateCookies if we should check for privacy. + aHosts[aHost] = aIsPinned; + } + else if (aScheme == "file") { + aHosts[aHost] = true; + } + }, + + /** + * Serialize cookie data + * @param aWindows + * JS object containing window data references + * { id: winData, etc. } + */ + _updateCookies: function sss_updateCookies(aWindows) { + var jscookies = {}; + // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision + var MAX_EXPIRY = Math.pow(2, 62); + + for (let window of aWindows) { + window.cookies = []; + + // Collect all hosts for the current window. + let hosts = {}; + window.tabs.forEach(function(tab) { + tab.entries.forEach(function(entry) { + this._extractHostsForCookiesFromEntry(entry, hosts, true, tab.pinned); + }, this); + }, this); + + for (var [host, isPinned] of Object.entries(hosts)) { + try { + var list = Services.cookies.getCookiesFromHost(host, {}); + while (list.hasMoreElements()) { + var cookie = list.getNext().QueryInterface(Ci.nsICookie2); + // window._hosts will only have hosts with the right privacy rules, + // so there is no need to do anything special with this call to + // _checkPrivacyLevel. + if (cookie.isSession && this._checkPrivacyLevel(cookie.isSecure, isPinned)) { + // use the cookie's host, path, and name as keys into a hash, + // to make sure we serialize each cookie only once + + // lazily build up a 3-dimensional hash, with + // host, path, and name as keys + if (!jscookies[cookie.host]) + jscookies[cookie.host] = {}; + if (!jscookies[cookie.host][cookie.path]) + jscookies[cookie.host][cookie.path] = {}; + + if (!jscookies[cookie.host][cookie.path][cookie.name]) { + var jscookie = { "host": cookie.host, "value": cookie.value }; + // only add attributes with non-default values (saving a few bits) + 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; + + jscookies[cookie.host][cookie.path][cookie.name] = jscookie; + } + window.cookies.push(jscookies[cookie.host][cookie.path][cookie.name]); + } + } + } + catch (ex) { + debug("getCookiesFromHost failed. Host: " + host); + } + } + + // don't include empty cookie sections + if (!window.cookies.length) + delete window.cookies; + } + }, + + /** + * Store window dimensions, visibility, sidebar + * @param aWindow + * Window reference + */ + _updateWindowFeatures: function sss_updateWindowFeatures(aWindow) { + var winData = this._windows[aWindow.__SSi]; + + for (var aAttr in WINDOW_ATTRIBUTES) + winData[aAttr] = this._getWindowDimension(aWindow, aAttr); + + 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; + }, + + /** + * serialize session data as Ini-formatted string + * @param aUpdateAll + * Bool update all windows + * @returns string + */ + _getCurrentState: function sss_getCurrentState(aUpdateAll) { + this._handleClosedWindows(); + + var activeWindow = this._getMostRecentBrowserWindow(); + + if (this._loadState == STATE_RUNNING) { + // 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(); + } + + // collect the data for all windows + var total = [], ids = []; + var nonPopupCount = 0; + var ix; + 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++; + } + this._updateCookies(total); + + // 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 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 (AppConstants.platform != "macosx" && + nonPopupCount == 0 && lastClosedWindowsCopy.length > 0 && + this._loadState == STATE_QUITTING) { + // 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) + } + + if (activeWindow) { + this.activeWindowSSiCache = activeWindow.__SSi || ""; + } + ix = ids.indexOf(this.activeWindowSSiCache); + // We don't want to restore focus to a minimized window. + if (ix != -1 && total[ix].sizemode == "minimized") + ix = -1; + + let session = { + state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR, + lastUpdate: Date.now(), + startTime: this._sessionStartTime, + recentCrashes: this._recentCrashes + }; + + return { + windows: total, + selectedWindow: ix + 1, + _closedWindows: lastClosedWindowsCopy, + session: session + }; + }, + + /** + * serialize session data for a window + * @param aWindow + * Window reference + * @returns string + */ + _getWindowState: function sss_getWindowState(aWindow) { + if (!this._isWindowLoaded(aWindow)) + return this._statesToRestore[aWindow.__SS_restoreID]; + + if (this._loadState == STATE_RUNNING) { + this._collectWindowData(aWindow); + } + + let windows = [this._windows[aWindow.__SSi]]; + this._updateCookies(windows); + + return { windows: windows }; + }, + + _collectWindowData: function sss_collectWindowData(aWindow) { + if (!this._isWindowLoaded(aWindow)) + return; + + // update the internal state data for this window + this._saveWindowHistory(aWindow); + this._updateTextAndScrollData(aWindow); + 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); + }, + +/* ........ Restoring Functionality .............. */ + + /** + * restore features to a single window + * @param aWindow + * Window reference + * @param aState + * JS object or its eval'able source + * @param aOverwriteTabs + * bool overwrite existing tabs w/ new ones + * @param aFollowUp + * bool this isn't the restoration of the first window + */ + restoreWindow: function sss_restoreWindow(aWindow, aState, aOverwriteTabs, aFollowUp) { + if (!aFollowUp) { + this.windowToFocus = aWindow; + } + // initialize window if necessary + if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) + this.onLoad(aWindow); + + try { + var root = typeof aState == "string" ? JSON.parse(aState) : aState; + if (!root.windows[0]) { + this._sendRestoreCompletedNotifications(); + return; // nothing to restore + } + } + catch (ex) { // invalid state object - don't restore anything + debug(ex); + this._sendRestoreCompletedNotifications(); + return; + } + + // We're not returning from this before we end up calling restoreHistoryPrecursor + // for this window, so make sure we send the SSWindowStateBusy event. + this._sendWindowStateEvent(aWindow, "Busy"); + + if (root._closedWindows) + this._closedWindows = root._closedWindows; + + var winData; + if (!aState.selectedWindow || aState.selectedWindow > aState.windows.length) { + aState.selectedWindow = 0; + } + // open new windows for all further window entries of a multi-window session + // (unless they don't contain any tab data) + 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 == aState.selectedWindow - 1) { + this.windowToFocus = window; + } + } + } + winData = root.windows[0]; + 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 (root._firstTabs && !aOverwriteTabs && winData.tabs.length == 1 && + (!winData.tabs[0].entries || winData.tabs[0].entries.length == 0)) { + winData.tabs = []; + } + + var tabbrowser = aWindow.getBrowser(); + var openTabCount = aOverwriteTabs ? 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.arrowScrollbox; + var smoothScroll = tabstrip.smoothScroll; + tabstrip.smoothScroll = false; + + // make sure that the selected tab won't be closed in order to + // prevent unnecessary flickering + if (aOverwriteTabs && tabbrowser.tabContainer.selectedIndex >= newTabCount) + tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1); + + for (var t = 0; t < newTabCount; t++) { + tabs.push(t < openTabCount ? + tabbrowser.tabs[t] : + // Ftr, SeaMonkey doesn't support animation (yet). + tabbrowser.addTab("about:blank")); + // when resuming at startup: add additionally requested pages to the end + if (!aOverwriteTabs && root._firstTabs) { + tabbrowser.moveTabTo(tabs[t], t); + } + } + + // 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 restoreHistoryPrecursor). + if (aOverwriteTabs) { + for (let i = 0; i < tabbrowser.tabs.length; i++) { + if (tabbrowser.browsers[i].__SS_restoreState) + this._resetTabRestoringState(tabbrowser.tabs[i]); + } + } + + // We want to set up a counter on the window that indicates how many tabs + // in this window are unrestored. This will be used in restoreNextTab to + // determine if gRestoreTabsProgressListener should be removed from the window. + // If we aren't overwriting existing tabs, then we want to add to the existing + // count in case there are still tabs restoring. + if (!aWindow.__SS_tabsToRestore) + aWindow.__SS_tabsToRestore = 0; + if (aOverwriteTabs) + aWindow.__SS_tabsToRestore = newTabCount; + else + aWindow.__SS_tabsToRestore += newTabCount; + + // 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 + for (t = openTabCount - 1; t >= newTabCount; t--) { + tabbrowser.removeTab(tabbrowser.tabs[t]); + } + + if (aOverwriteTabs) { + this.restoreWindowFeatures(aWindow, winData); + delete this._windows[aWindow.__SSi].extData; + } + if (winData.cookies) { + this.restoreCookies(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]; + } + } + if (aOverwriteTabs || root._firstTabs) { + this._windows[aWindow.__SSi]._closedTabs = winData._closedTabs || []; + } + + this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs, + (aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0); + + // set smoothScroll back to the original value + tabstrip.smoothScroll = smoothScroll; + + this._sendRestoreCompletedNotifications(); + }, + + /** + * 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 selected tab + * @param aIx + * Index of the next tab to check readyness for + * @param aCount + * Counter for number of times delaying b/c browser or history aren't ready + */ + restoreHistoryPrecursor: + function sss_restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount) { + var tabbrowser = aWindow.getBrowser(); + + // make sure that all browsers and their histories are available + // - if one's not, resume this check in 100ms (repeat at most 10 times) + for (var t = aIx; t < aTabs.length; t++) { + try { + if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) { + throw new Error(); + } + } + catch (ex) { // in case browser or history aren't ready yet + if (aCount < 10) { + var restoreHistoryFunc = function(self) { + self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount + 1); + } + aWindow.setTimeout(restoreHistoryFunc, 100, this); + return; + } + } + } + + 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; + + // 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(aWindow); + } + + if (aTabs.length == 0) { + // this is normally done in restoreHistory() but as we're returning early + // here we need to take care of it. + this._sendWindowStateEvent(aWindow, "Ready"); + return; + } + + if (aTabs.length > 1) { + // Load hidden tabs last, by pushing them to the end of the list + let unhiddenTabs = aTabs.length; + for (let t = 0; t < unhiddenTabs; ) { + if (aTabData[t].hidden) { + aTabs = aTabs.concat(aTabs.splice(t, 1)); + aTabData = aTabData.concat(aTabData.splice(t, 1)); + if (aSelectTab > t) + --aSelectTab; + --unhiddenTabs; + continue; + } + ++t; + } + + // Determine if we can optimize & load visible tabs first + let maxVisibleTabs = Math.ceil(tabbrowser.tabContainer.arrowScrollbox.scrollClientSize / + aTabs[unhiddenTabs - 1].getBoundingClientRect().width); + + // make sure we restore visible tabs first, if there are enough + if (maxVisibleTabs < unhiddenTabs && aSelectTab > 1) { + let firstVisibleTab = 0; + if (unhiddenTabs - maxVisibleTabs > aSelectTab) { + // aSelectTab is leftmost since we scroll to it when possible + firstVisibleTab = aSelectTab - 1; + } else { + // aSelectTab is rightmost or no more room to scroll right + firstVisibleTab = unhiddenTabs - maxVisibleTabs; + } + aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs); + aTabData = aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData); + aSelectTab -= firstVisibleTab; + } + } + + // make sure to restore the selected tab first (if any) + if (aSelectTab-- && aTabs[aSelectTab]) { + aTabs.unshift(aTabs.splice(aSelectTab, 1)[0]); + aTabData.unshift(aTabData.splice(aSelectTab, 1)[0]); + tabbrowser.selectedTab = aTabs[0]; + } + + // Prepare the tabs so that they can be properly restored. We'll pin/unpin + // and show/hide tabs as necessary. We'll also set the labels, user typed + // value, and attach a copy of the tab's data in case we close it before + // it's been restored. + for (t = 0; t < aTabs.length; t++) { + let tab = aTabs[t]; + let browser = tabbrowser.getBrowserForTab(tab); + let tabData = aTabData[t]; + + if (tabData.hidden) { + tab.setAttribute("hidden", true); + } else { + if (tab.hidden) { + tab.removeAttribute("hidden"); + } + } + + for (let name in tabData.attributes) + this.xulAttributes[name] = true; + + // keep the data around to prevent dataloss in case + // a tab gets closed before it's been properly restored + browser.__SS_data = tabData; + browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE; + + // Make sure that set/getTabValue will set/read the correct data by + // wiping out any current value in tab.__SS_extdata. + delete tab.__SS_extdata; + + if (!tabData.entries || tabData.entries.length == 0) { + // make sure to blank out this tab's content + // (just purging the tab's history won't be enough) + browser.contentDocument.location = "about:blank"; + continue; + } + + browser.stop(); // in case about:blank isn't done yet + + // wall-paper fix for bug 439675: make sure that the URL to be loaded + // is always visible in the address bar + let activeIndex = (tabData.index || tabData.entries.length) - 1; + let activePageData = tabData.entries[activeIndex] || null; + let uri = activePageData ? activePageData.url || null : null; + + // NB: we won't set initial URIs (about:blank, about:privatebrowsing, 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. + if (!browser.userTypedValue && uri && !aWindow.gInitialPages.has(uri)) { + browser.userTypedValue = uri; + } + + // Also 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. + if (uri) + browser.docShell.setCurrentURI(this._getURIFromString(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"; + } + } + } + + // helper hashes for ensuring unique frame IDs and unique document + // identifiers. + var idMap = { used: {} }; + var docIdentMap = {}; + this.restoreHistory(aWindow, aTabs, aTabData, idMap, docIdentMap); + }, + + /** + * Restore history for a window + * @param aWindow + * Window reference + * @param aTabs + * Array of tab references + * @param aTabData + * Array of tab data + * @param aIdMap + * Hash for ensuring unique frame IDs + */ + restoreHistory: + function sss_restoreHistory(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap) { + // if the tab got removed before being completely restored, then skip it + while (aTabs.length > 0 && (!aTabs[0].parentNode || !aTabs[0].linkedBrowser)) { + aTabs.shift(); + aTabData.shift(); + } + if (aTabs.length == 0) { + // At this point we're essentially ready for consumers to read/write data + // via the sessionstore API so we'll send the SSWindowStateReady event. + this._sendWindowStateEvent(aWindow, "Ready"); + return; // no more tabs to restore + } + + var tab = aTabs.shift(); + var tabData = aTabData.shift(); + + var browser = aWindow.getBrowser().getBrowserForTab(tab); + var history = browser.webNavigation.sessionHistory; + + if (history.count > 0) { + history.PurgeHistory(history.count); + } + + browser.__SS_shistoryListener = new SessionStoreSHistoryListener(this, tab); + history.addSHistoryListener(browser.__SS_shistoryListener); + + if (!tabData.entries) { + tabData.entries = []; + } + if (tabData.extData) { + tab.__SS_extdata = {}; + for (let key in tabData.extData) + tab.__SS_extdata[key] = tabData.extData[key]; + } + else + delete tab.__SS_extdata; + + for (var i = 0; i < tabData.entries.length; i++) { + let cloneEntry = false; + //XXXzpao Wallpaper patch for bug 509315 + if (!tabData.entries[i].url) + continue; + + let shEntry = this._deserializeHistoryEntry(tabData.entries[i], + aIdMap, aDocIdentMap); + try { + history.addEntry(shEntry, true); + } + catch (ex) { + cloneEntry = true; + } + + // Workaround for bug 1466911. + // FIXME Remove this after the issue which caused the exception above + // to be thrown has been fixed. + if (cloneEntry) { + shEntry = shEntry.clone(); + shEntry.abandonBFCacheEntry(); + + try { + history.addEntry(shEntry, true); + } + catch (ex) { + Cu.reportError(ex); + } + } + } + + // make sure to reset the capabilities and attributes, in case this tab gets reused + var disallow = (tabData.disallow)?tabData.disallow.split(","):[]; + CAPABILITIES.forEach(function(aCapability) { + browser.docShell["allow" + aCapability] = !disallow.includes(aCapability); + }); + for (let name in this.xulAttributes) + tab.removeAttribute(name); + for (let name in tabData.attributes) + tab.setAttribute(name, tabData.attributes[name]); + + if (tabData.storage && browser.docShell instanceof Ci.nsIDocShell) + this._deserializeSessionStorage(tabData.storage, browser.docShell); + + // notify the tabbrowser that the tab chrome has been restored + var event = aWindow.document.createEvent("Events"); + event.initEvent("SSTabRestoring", true, false); + tab.dispatchEvent(event); + + // Restore the history in the next tab + Services.tm.mainThread.dispatch(this.restoreHistory.bind(this, aWindow, + aTabs, aTabData, aIdMap, aDocIdentMap), Ci.nsIThread.DISPATCH_NORMAL); + + // This could cause us to ignore the max_concurrent_tabs pref a bit, but + // it ensures each window will have its selected tab loaded. + if (aWindow.getBrowser().selectedBrowser == browser) { + this.restoreTab(tab); + } + else { + // Put the tab into the right bucket + if (tabData.hidden) + this._tabsToRestore.hidden.push(tab); + else + this._tabsToRestore.visible.push(tab); + this.restoreNextTab(); + } + }, + + /** + * Restores the specified tab. If the tab can't be restored (eg, no history or + * calling gotoIndex fails), then state changes will be rolled back. + * This method will check if gTabsProgressListener is attached to the tab's + * window, ensuring that we don't get caught without one. + * This method removes the session history listener right before starting to + * attempt a load. This will prevent cases of "stuck" listeners. + * If this method returns false, then it is up to the caller to decide what to + * do. In the common case (restoreNextTab), we will want to then attempt to + * restore the next tab. In the other case (selecting the tab, reloading the + * tab), the caller doesn't actually want to do anything if no page is loaded. + * + * @param aTab + * the tab to restore + * + * @returns true/false indicating whether or not a load actually happened + */ + restoreTab: function sss_restoreTab(aTab) { + let window = aTab.ownerDocument.defaultView; + let browser = aTab.linkedBrowser; + let tabData = browser.__SS_data; + + // If the tabData which we're sending down has any sessionStorage associated + // with it, we need to send down permissions for the domains, as this + // information will be needed to correctly restore the session. + if (tabData.storage) { + for (let origin of Object.getOwnPropertyNames(tabData.storage)) { + try { + let {frameLoader} = browser; + if (frameLoader.tabParent) { + let attrs = browser.contentPrincipal.originAttributes; + let dataPrincipal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin); + let principal = Services.scriptSecurityManager.createCodebasePrincipal(dataPrincipal.URI, attrs); + frameLoader.tabParent.transmitPermissionsForPrincipal(principal); + } + } catch (e) { + Cu.reportError(e); + } + } + } + + // There are cases within where we haven't actually started a load. In that + // that case we'll reset state changes we made and return false to the caller + // can handle appropriately. + let didStartLoad = false; + + // Make sure that the tabs progress listener is attached to this window + this._ensureTabsProgressListener(window); + + // Make sure that this tab is removed from _tabsToRestore + this._removeTabFromTabsToRestore(aTab); + + // Increase our internal count. + this._tabsRestoringCount++; + + // Set this tab's state to restoring + browser.__SS_restoreState = TAB_STATE_RESTORING; + + // Remove the history listener, since we no longer need it once we start restoring + this._removeSHistoryListener(aTab); + + let activeIndex = (tabData.index || tabData.entries.length) - 1; + if (activeIndex >= tabData.entries.length) + activeIndex = tabData.entries.length - 1; + + // Reset currentURI. + browser.webNavigation.setCurrentURI(this._getURIFromString("about:blank")); + + // Attach data that will be restored on "load" event, after tab is restored. + if (activeIndex > -1) { + // restore those aspects of the currently active documents which are not + // preserved in the plain history entries (mainly scroll state and text data) + browser.__SS_restore_data = tabData.entries[activeIndex] || {}; + browser.__SS_restore_pageStyle = tabData.pageStyle || ""; + browser.__SS_restore_tab = aTab; + + didStartLoad = true; + try { + // 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. + var sessionHistory = browser.webNavigation.sessionHistory; + sessionHistory.index = activeIndex; + sessionHistory.reloadCurrentEntry(); + } + catch (ex) { + // ignore page load errors + aTab.removeAttribute("busy"); + didStartLoad = false; + } + } + + // Handle userTypedValue. Setting userTypedValue seems to update gURLbar + // as needed. Calling loadURI will cancel form filling in restoreDocument + if (tabData.userTypedValue) { + browser.userTypedValue = tabData.userTypedValue; + + if (tabData.userTypedClear) { + // Make it so that we'll enter restoreDocument on page load. We will + // fire SSTabRestored from there. We don't have any form data to + // restore so we can just set the URL to null. + browser.__SS_restore_data = { url: null }; + browser.__SS_restore_tab = aTab; + didStartLoad = true; + browser.webNavigation + .loadURI(tabData.userTypedValue, + Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, + null, null, null, + Services.scriptSecurityManager.getSystemPrincipal()); + } + } + + // If we didn't start a load, then we won't reset this tab through the usual + // channel (via the progress listener), so reset the tab ourselves. We will + // also send SSTabRestored since this tab has technically been restored. + if (!didStartLoad) { + this._sendTabRestoredNotification(aTab); + this._resetTabRestoringState(aTab); + } + + return didStartLoad; + }, + + /** + * 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 sss_restoreNextTab() { + // If we call in here while quitting, we don't actually want to do anything + if (this._loadState == STATE_QUITTING) + return; + + // If it's not possible to restore anything, then just bail out. + if (this._maxConcurrentTabRestores >= 0 && + this._tabsRestoringCount >= this._maxConcurrentTabRestores) + return; + + // Look in visible, then hidden + let nextTabArray; + if (this._tabsToRestore.visible.length) { + nextTabArray = this._tabsToRestore.visible; + } + else if (this._tabsToRestore.hidden.length) { + nextTabArray = this._tabsToRestore.hidden; + } + + if (nextTabArray) { + let tab = nextTabArray.shift(); + let didStartLoad = this.restoreTab(tab); + // If we don't start a load in the restored tab (eg, no entries) then we + // want to attempt to restore the next tab. + if (!didStartLoad) + this.restoreNextTab(); + } + }, + + /** + * expands serialized history data into a session-history-entry instance + * @param aEntry + * Object containing serialized history data for a URL + * @param aIdMap + * Hash for ensuring unique frame IDs + * @returns nsISHEntry + */ + _deserializeHistoryEntry: + function sss_deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) { + + var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"] + .createInstance(Ci.nsISHEntry); + + shEntry.URI = this._getURIFromString(aEntry.url); + shEntry.title = aEntry.title || aEntry.url; + if (aEntry.subframe) + shEntry.isSubFrame = aEntry.subframe || false; + shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; + if (aEntry.contentType) + shEntry.contentType = aEntry.contentType; + if (aEntry.referrer) + shEntry.referrerURI = this._getURIFromString(aEntry.referrer); + + if (aEntry.cacheKey) { + var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"] + .createInstance(Ci.nsISupportsPRUint32); + cacheKey.data = aEntry.cacheKey; + shEntry.cacheKey = cacheKey; + } + + if (aEntry.ID) { + // get a new unique ID for this frame (since the one from the last + // start might already be in use) + var id = aIdMap[aEntry.ID] || 0; + if (!id) { + for (id = Date.now(); id in aIdMap.used; id++); + aIdMap[aEntry.ID] = id; + aIdMap.used[id] = true; + } + shEntry.ID = id; + } + + // If we have the legacy docshellID on our entry, upgrade it to a + // docshellUUID by going through the mapping. + if (aEntry.docshellID) { + if (!this._docshellUUIDMap.has(aEntry.docshellID)) { + // Convert the nsID to a string so that the docshellUUID property + // is correctly stored as a string. + this._docshellUUIDMap.set(aEntry.docshellID, + uuidGenerator.generateUUID().toString()); + } + aEntry.docshellUUID = this._docshellUUIDMap.get(aEntry.docshellID); + delete aEntry.docshellID; + } + + if (aEntry.docshellUUID) + shEntry.docshellID = Components.ID(aEntry.docshellUUID); + + if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) { + shEntry.stateData = + Cc["@mozilla.org/docshell/structured-clone-container;1"] + .createInstance(Ci.nsIStructuredCloneContainer); + + shEntry.stateData.initFromBase64(aEntry.structuredCloneState, + aEntry.structuredCloneVersion); + } + + if (aEntry.scroll) { + var scrollPos = (aEntry.scroll || "0,0").split(","); + scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; + shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); + } + + if (aEntry.postdata_b64) { + var postdata = atob(aEntry.postdata_b64); + var stream = Cc["@mozilla.org/io/string-input-stream;1"] + .createInstance(Ci.nsIStringInputStream); + stream.setData(postdata, postdata.length); + shEntry.postData = stream; + } + + let childDocIdents = {}; + if (aEntry.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 = aDocIdentMap[aEntry.docIdentifier]; + if (!matchingEntry) { + matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; + aDocIdentMap[aEntry.docIdentifier] = matchingEntry; + } + else { + shEntry.adoptBFCacheEntry(matchingEntry.shEntry); + childDocIdents = matchingEntry.childDocIdents; + } + } + + // The field entry.owner_b64 got renamed to entry.triggeringPricipal_b64 in + // Bug 1286472 and Bug 1334780 for SeaMonkey. + // To remain backward compatible we still have to support that field for a + // few cycles before we can remove it. + if (aEntry.owner_b64) { + aEntry.triggeringPricipal_b64 = aEntry.owner_b64; + delete aEntry.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 (aEntry.triggeringPrincipal_base64 || aEntry.principalToInherit_base64) { + if (aEntry.triggeringPrincipal_base64) { + shEntry.triggeringPrincipal = + Utils.deserializePrincipal(aEntry.triggeringPrincipal_base64); + } + if (aEntry.principalToInherit_base64) { + shEntry.principalToInherit = + Utils.deserializePrincipal(aEntry.principalToInherit_base64); + } + } else if (aEntry.triggeringPrincipal_b64) { + shEntry.triggeringPrincipal = Utils.deserializePrincipal(aEntry.triggeringPrincipal_b64); + shEntry.principalToInherit = shEntry.triggeringPrincipal; + } + + if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { + for (var i = 0; i < aEntry.children.length; i++) { + //XXXzpao Wallpaper patch for bug 509315 + if (!aEntry.children[i].url) + continue; + + // We're mysteriously getting sessionrestore.js files with a cycle in + // the doc-identifier graph. (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._deserializeHistoryEntry(aEntry.children[i], aIdMap, + childDocIdents), i); + } + } + + return shEntry; + }, + + /** + * restores all sessionStorage "super cookies" + * @param aStorageData + * Storage data to be restored + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + */ + _deserializeSessionStorage: function sss_deserializeSessionStorage(aStorageData, aDocShell) { + + for (let origin of Object.keys(aStorageData)) { + let data = aStorageData[origin]; + + let principal; + + try { + // NOTE: We record the full origin for the URI which the + // sessionStorage is being captured for. As of bug 1319114 this code + // stopped parsing any origins which have originattributes correctly, as + // it decided to use the origin attributes from the docshell, and try to + // interpret the origin as a URI. Since bug 1473426 code now correctly + // parses the full origin, and then discards the origin attributes, to + // make the behavior line up with the original intentions in bug 1235657 + // while preserving the ability to read all session storage from + // previous versions. In the future, if this behavior is desired, we may + // want to use the spec instead of the origin as the key, and avoid + // transmitting origin attribute information which we then discard when + // restoring. + // + // If changing this logic, make sure to also change the principal + // computation logic in restoretab. + let attrs = aDocShell.getOriginAttributes(); + let dataPrincipal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin); + principal = Services.scriptSecurityManager.createCodebasePrincipal(dataPrincipal.URI, attrs); + } catch (e) { + Cu.reportError(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, ""); + + 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. + Cu.reportError(e); + } + } + } + }, + + /** + * Restore properties to a loaded document + */ + restoreDocument: function sss_restoreDocument(aWindow, aBrowser, aEvent) { + // wait for the top frame to be loaded completely + if (!aEvent || !aEvent.originalTarget || !aEvent.originalTarget.defaultView || aEvent.originalTarget.defaultView != aEvent.originalTarget.defaultView.top) { + return; + } + + // always call this before injecting content into a document! + function hasExpectedURL(aDocument, aURL) { + return !aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, ""); + } + + function restoreFormData(aDocument, aData, aURL) { + for (let key in aData) { + if (!hasExpectedURL(aDocument, aURL)) + return; + + let node = key.charAt(0) == "#" ? aDocument.getElementById(key.slice(1)) : + XPathGenerator.resolve(aDocument, key); + if (!node) + continue; + + let eventType; + let value = aData[key]; + if (typeof value == "string" && node.type != "file") { + if (node.value == value) + continue; // don't dispatch an input event for no change + + node.value = value; + eventType = "input"; + } + else if (typeof value == "boolean") { + if (node.checked == value) + continue; // don't dispatch a change event for no change + + node.checked = value; + eventType = "change"; + } + else if (typeof value == "number") { + // We saved the value blindly since selects take more work to determine + // default values. So now we should check to avoid unnecessary events. + if (node.selectedIndex == value) + continue; + + try { + node.selectedIndex = value; + eventType = "change"; + } catch (ex) { /* throws for invalid indices */ } + } + else if (value && value.fileList && value.type == "file" && node.type == "file") { + node.mozSetFileNameArray(value.fileList, value.fileList.length); + eventType = "input"; + } + else if (value && typeof value.indexOf == "function" && node.options) { + Array.from(node.options).forEach(function(aOpt, aIx) { + aOpt.selected = value.includes(aIx); + + // Only fire the event here if this wasn't selected by default + if (!aOpt.defaultSelected) + eventType = "change"; + }); + } + + // Fire events for this node if applicable + if (eventType) { + let event = aDocument.createEvent("UIEvents"); + event.initUIEvent(eventType, true, true, aDocument.defaultView, 0); + node.dispatchEvent(event); + } + } + } + + let selectedPageStyle = aBrowser.__SS_restore_pageStyle; + function restoreTextDataAndScrolling(aContent, aData, aPrefix) { + if (aData.formdata) + restoreFormData(aContent.document, aData.formdata, aData.url); + if (aData.innerHTML) { + aWindow.setTimeout(function() { + if (aContent.document.designMode == "on" && + hasExpectedURL(aContent.document, aData.url)) { + aContent.document.body.innerHTML = aData.innerHTML; + } + }, 0); + } + var match; + if (aData.scroll && (match = /(\d+),(\d+)/.exec(aData.scroll)) != null) { + aContent.scrollTo(match[1], match[2]); + } + Array.from(aContent.document.styleSheets).forEach(function(aSS) { + aSS.disabled = aSS.title && aSS.title != selectedPageStyle; + }); + for (var i = 0; i < aContent.frames.length; i++) { + if (aData.children && aData.children[i] && + hasExpectedURL(aContent.document, aData.url)) { + restoreTextDataAndScrolling(aContent.frames[i], aData.children[i], aPrefix + i + "|"); + } + } + } + + // don't restore text data and scrolling state if the user has navigated + // away before the loading completed (except for in-page navigation) + if (hasExpectedURL(aEvent.originalTarget, aBrowser.__SS_restore_data.url)) { + var content = aEvent.originalTarget.defaultView; + restoreTextDataAndScrolling(content, aBrowser.__SS_restore_data, ""); + aBrowser.markupDocumentViewer.authorStyleDisabled = selectedPageStyle == "_nostyle"; + } + + // notify the tabbrowser that this document has been completely restored + this._sendTabRestoredNotification(aBrowser.__SS_restore_tab); + + delete aBrowser.__SS_restore_data; + delete aBrowser.__SS_restore_pageStyle; + delete aBrowser.__SS_restore_tab; + }, + + /** + * Restore visibility and dimension features to a window + * @param aWindow + * Window reference + * @param aWinData + * Object containing session data for the window + */ + restoreWindowFeatures: function sss_restoreWindowFeatures(aWindow, aWinData) { + var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[]; + WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) { + aWindow[aItem].visible = !hidden.includes(aItem); + }); + + if (aWinData.isPopup) + this._windows[aWindow.__SSi].isPopup = true; + else + delete this._windows[aWindow.__SSi].isPopup; + + Services.tm.mainThread.dispatch(this.restoreDimensions.bind(this, aWindow, + +aWinData.width || 0, + +aWinData.height || 0, + "screenX" in aWinData ? +aWinData.screenX : NaN, + "screenY" in aWinData ? +aWinData.screenY : NaN, + aWinData.sizemode || "", aWinData.sidebar || ""), + Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** + * 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 sss_restoreDimensions(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) { + var win_ = this._getWindowDimension.bind(this, aWindow); + + // 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); + // constrain the dimensions to the actual space available + if (aWidth > screenWidth.value) { + aWidth = screenWidth.value; + } + if (aHeight > screenHeight.value) { + aHeight = screenHeight.value; + } + // and then pull the window within the screen's bounds + if (aLeft < screenLeft.value) { + aLeft = screenLeft.value; + } else if (aLeft + aWidth > screenLeft.value + screenWidth.value) { + aLeft = screenLeft.value + screenWidth.value - aWidth; + } + if (aTop < screenTop.value) { + aTop = screenTop.value; + } else if (aTop + aHeight > screenTop.value + screenHeight.value) { + aTop = screenTop.value + screenHeight.value - aHeight; + } + } + + // only modify those aspects which aren't correct yet + if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) { + aWindow.resizeTo(aWidth, aHeight); + } + if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) { + aWindow.moveTo(aLeft, aTop); + } + 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.toggleSidebar(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.content) { + this.windowToFocus.content.focus(); + } + }, + + /** + * Restores cookies + * @param aCookies + * Array of cookie objects + */ + restoreCookies: function sss_restoreCookies(aCookies) { + // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision + var MAX_EXPIRY = Math.pow(2, 62); + for (let i = 0; i < aCookies.length; i++) { + var cookie = aCookies[i]; + try { + Services.cookies.add(cookie.host, cookie.path || "", cookie.name || "", + cookie.value, !!cookie.secure, !!cookie.httponly, + true, + "expiry" in cookie ? cookie.expiry : MAX_EXPIRY, + "originAttributes" in cookie ? cookie.originAttributes : {}); + } + catch (ex) { Cu.reportError(ex); } // don't let a single cookie stop recovering + } + }, + +/* ........ Disk Access .............. */ + + /** + * save state delayed by N ms + * marks window as dirty (i.e. data update can't be skipped) + * @param aWindow + * Window reference + * @param aDelay + * Milliseconds to delay + */ + saveStateDelayed: function sss_saveStateDelayed(aWindow, aDelay) { + if (aWindow) { + DirtyWindows.add(aWindow); + } + + if (!this._saveTimer && this._resume_from_crash) { + // interval until the next disk operation is allowed + var minimalDelay = this._lastSaveTime + this._interval - Date.now(); + + // if we have to wait, set a timer, otherwise saveState directly + aDelay = Math.max(minimalDelay, aDelay || 2000); + if (aDelay > 0) { + this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT); + } + else { + this.saveState(); + } + } + }, + + /** + * save state to disk + * @param aUpdateAll + * Bool update all windows + */ + saveState: function sss_saveState(aUpdateAll) { + // if crash recovery is disabled, only save session resuming information + if (!this._resume_from_crash && this._loadState == STATE_RUNNING) + return; + + // If crash recovery is disabled, we only want to resume with pinned tabs + // if we crash. + let pinnedOnly = this._loadState == STATE_RUNNING && !this._resume_from_crash; + + var oState = this._getCurrentState(aUpdateAll); + if (!oState) + return; + + // Persist the last session if we deferred restoring it + if (this._lastSessionState) + oState.lastSessionState = this._lastSessionState; + + this._saveStateObject(oState); + }, + + /** + * write a state object to disk + */ + _saveStateObject: function sss_saveStateObject(aStateObj) { + var stateString = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + // parentheses are for backwards compatibility with older sessionstore files + stateString.data = this._toJSONString(aStateObj); + + Services.obs.notifyObservers(stateString, "sessionstore-state-write"); + + // don't touch the file if an observer has deleted all state data + if (stateString.data) + this._writeFile(this._sessionFile, stateString.data); + + this._lastSaveTime = Date.now(); + }, + + /** + * delete session datafile and backup + */ + _clearDisk: function sss_clearDisk() { + if (this._sessionFile.exists()) { + try { + this._sessionFile.remove(false); + } + catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now? + } + if (this._sessionFileBackup.exists()) { + try { + this._sessionFileBackup.remove(false); + } + catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now? + } + }, + +/* ........ Auxiliary Functions .............. */ + + /** + * 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 sss_forEachBrowserWindow(aFunc) { + var windowsEnum = Services.wm.getEnumerator("navigator:browser"); + + while (windowsEnum.hasMoreElements()) { + var window = windowsEnum.getNext(); + if (!window.closed && window.__SSi) { + aFunc.call(this, window); + } + } + }, + + /** + * Returns most recent window + * @returns Window reference + */ + _getMostRecentBrowserWindow: function sss_getMostRecentBrowserWindow() { + var win = Services.wm.getMostRecentWindow("navigator:browser"); + if (!win) + return null; + if (!win.closed) + return win; + + let broken_wm_z_order = + AppConstants.platform != "macosx" && AppConstants.platform != "win"; + + if (broken_wm_z_order) { + win = null; + var windowsEnum = Services.wm.getEnumerator("navigator:browser"); + // this is oldest to newest, so this gets a bit ugly + while (windowsEnum.hasMoreElements()) { + let nextWin = windowsEnum.getNext(); + if (!nextWin.closed) + win = nextWin; + } + return win; + } + + var windowsEnum = + Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true); + + while (windowsEnum.hasMoreElements()) { + win = windowsEnum.getNext(); + if (!win.closed) + return win; + } + + return null; + }, + + /** + * 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 sss_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 sss_openWindowWithState(aState) { + var argString = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + argString.data = "about:blank"; + + var features = "chrome,dialog=no,suppressanimation,all"; + var winState = aState.windows[0]; + for (var aAttr in WINDOW_ATTRIBUTES) { + // Use !isNaN as an easy way to ignore sizemode and check for numbers + if (aAttr in winState && !isNaN(winState[aAttr])) + features += "," + WINDOW_ATTRIBUTES[aAttr] + "=" + winState[aAttr]; + } + + 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; + }, + + /** + * Gets the tab for the given browser. This should be marginally better + * than using tabbrowser's getTabForContentWindow. This assumes the browser + * is the linkedBrowser of a tab, not a dangling browser. + * + * @param aBrowser + * The browser from which to get the tab. + */ + _getTabForBrowser: function sss_getTabForBrowser(aBrowser) { + let windowTabs = aBrowser.ownerDocument.defaultView.getBrowser().tabs; + for (let i = 0; i < windowTabs.length; i++) { + let tab = windowTabs[i]; + if (tab.linkedBrowser == aBrowser) + return tab; + } + }, + + /** + * Whether or not to resume session, if not recovering from a crash. + * @returns bool + */ + _doResumeSession: function sss_doResumeSession() { + return this._prefBranch.getIntPref("startup.page") == 3 || + this._prefBranch.getBoolPref("sessionstore.resume_session_once"); + }, + + /** + * Are we restarting to switch profile. + * @returns bool + */ + _isSwitchingProfile: function sss_isSwitchingProfile() { + var env = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); + return env.exists("XRE_PROFILE_NAME"); + }, + + /** + * 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 sss_isCmdLineEmpty(aWindow) { + return "arguments" in aWindow && aWindow.arguments.length && + aWindow.arguments[0] == "about:blank"; + }, + + /** + * don't save sensitive data if the user doesn't want to + * (distinguishes between encrypted and non-encrypted sites) + * @param aIsHTTPS + * Bool is encrypted + * @param aUseDefaultPref + * don't do normal check for deferred + * @returns bool + */ + _checkPrivacyLevel: function sss_checkPrivacyLevel(aIsHTTPS, aUseDefaultPref) { + let pref = "sessionstore.privacy_level"; + // If we're in the process of quitting and we're not autoresuming the session + // then we should treat it as a deferred session. We have a different privacy + // pref for that case. + if (!aUseDefaultPref && this._loadState == STATE_QUITTING && !this._doResumeSession()) + pref = "sessionstore.privacy_level_deferred"; + return this._prefBranch.getIntPref(pref) < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL); + }, + + /** + * 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 sss_getWindowDimension(aWindow, aAttribute) { + var dimension = aWindow[WINDOW_ATTRIBUTES[aAttribute]]; + if (aAttribute == "sizemode") { + switch (dimension) { + case aWindow.STATE_MAXIMIZED: + return "maximized"; + case aWindow.STATE_MINIMIZED: + return "minimized"; + default: + return "normal"; + } + } + + if (aWindow.windowState == aWindow.STATE_NORMAL) { + return dimension; + } + return aWindow.document.documentElement.getAttribute(aAttribute) || dimension; + }, + + /** + * Get nsIURI from string + * @param string + * @returns nsIURI + */ + _getURIFromString: function sss_getURIFromString(aString) { + return Services.io.newURI(aString); + }, + + /** + * Annotate a breakpad crash report with the currently selected tab's URL. + */ + _updateCrashReportURL: function sss_updateCrashReportURL(aWindow) { + + // If the crash reporter isn't built, we bail out. + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + try { + var currentURI = aWindow.getBrowser().currentURI.clone(); + // if the current URI contains a username/password, remove it + try { + currentURI.userPass = ""; + } + catch (ex) { } // ignore failures on about: URIs + + Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsICrashReporter) + .annotateCrashReport("URL", currentURI.spec); + } + catch (ex) { + // don't make noise when crashreporter is built but not enabled + if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) + debug(ex); + } + }, + + /** + * @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 sss_needsRestorePage(aState, aRecentCrashes) { + const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000; + + // don't display the page when there's nothing to restore + if (!aState.windows || !aState.windows.length) + return false; + + // don't wrap a single about:sessionrestore page + let winData = aState.windows; + if (winData.length == 1 && winData[0].tabs && + winData[0].tabs.length == 1 && winData[0].tabs[0].entries && + winData[0].tabs[0].entries.length == 1 && + winData[0].tabs[0].entries[0].url == "about:sessionrestore") + 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); + }, + + /** + * 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 sss__shouldSaveTabState(aTabState) { + // If the tab has only the transient about:blank 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.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 placed into + * this._lastSessionState 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 sss__prepDataForDeferredRestore(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 sss_splitCookiesFromWindow(aWinState, aTargetWinState) { + if (!aWinState.cookies || !aWinState.cookies.length) + return; + + // Get the hosts for history entries in aTargetWinState + let cookieHosts = {}; + aTargetWinState.tabs.forEach(function(tab) { + tab.entries.forEach(function(entry) { + this._extractHostsForCookiesFromEntry(entry, cookieHosts, false); + }, this); + }, this); + + // 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, "\\."); + 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++; + } + }, + + /** + * Converts a JavaScript object into a JSON string + * (see http://www.json.org/ for more information). + * + * The inverse operation consists of JSON.parse(JSON_string). + * + * @param aJSObject is the object to be converted + * @returns the object's JSON representation + */ + _toJSONString: function sss_toJSONString(aJSObject) { + return JSON.stringify(aJSObject); + }, + + _sendRestoreCompletedNotifications: function sss_sendRestoreCompletedNotifications() { + // not all windows restored, yet + if (this._restoreCount > 1) { + this._restoreCount--; + return; + } + + // observers were already notified + if (this._restoreCount == -1) + return; + + Services.tm.mainThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL); + + this._restoreCount = -1; + }, + + run: function sss_run() { + // This was the last window restored at startup, notify observers. + Services.obs.notifyObservers(this.windowToFocus, + this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED); + this._browserSetState = false; + }, + + /** + * 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 sss_sendWindowStateEvent(aWindow, aType) { + let event = aWindow.document.createEvent("Events"); + event.initEvent("SSWindowState" + aType, true, false); + aWindow.dispatchEvent(event); + }, + + /** + * Dispatch the SSTabRestored event for the given tab. + * @param aTab the which has been restored + */ + _sendTabRestoredNotification: function sss_sendTabRestoredNotification(aTab) { + let event = aTab.ownerDocument.createEvent("Events"); + event.initEvent("SSTabRestored", true, false); + 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 sss_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 sss_replaceLoadingTitle(aString, aTabbrowser, aTab) { + if (aString == aTabbrowser.mStringBundle.getString("tabs.loading")) { + 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 sss_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); + }, + + /** + * Reset state to prepare for a new session state to be restored. + */ + _resetRestoringState: function sss_initRestoringState() { + this._tabsToRestore = { visible: [], hidden: [] }; + 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" + */ + _resetTabRestoringState: function sss_resetTabRestoringState(aTab) { + let window = aTab.ownerDocument.defaultView; + 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; + + // We want to decrement window.__SS_tabsToRestore here so that we always + // decrement it AFTER a tab is done restoring or when a tab gets "reset". + window.__SS_tabsToRestore--; + + // Remove the progress listener if we should. + this._removeTabsProgressListener(window); + + if (previousState == TAB_STATE_RESTORING) { + if (this._tabsRestoringCount) + this._tabsRestoringCount--; + } + else if (previousState == TAB_STATE_NEEDS_RESTORE) { + // Make sure the session history listener is removed. This is normally + // done in restoreTab, but this tab is being removed before that gets called. + this._removeSHistoryListener(aTab); + + // Make sure that the tab is removed from the list of tabs to restore. + // Again, this is normally done in restoreTab, but that isn't being called + // for this tab. + this._removeTabFromTabsToRestore(aTab); + } + }, + + /** + * Remove the tab from this._tabsToRestore[visible/hidden] + * + * @param aTab + */ + _removeTabFromTabsToRestore: function sss_removeTabFromTabsToRestore(aTab) { + let arr = this._tabsToRestore[aTab.hidden ? "hidden" : "visible"]; + let index = arr.indexOf(aTab); + if (index > -1) + arr.splice(index, 1); + }, + + /** + * Add the tabs progress listener to the window if it isn't already + * + * @param aWindow + * The window to add our progress listener to + */ + _ensureTabsProgressListener: function sss_ensureTabsProgressListener(aWindow) { + let tabbrowser = aWindow.getBrowser(); + try { + tabbrowser.addTabsProgressListener(gRestoreTabsProgressListener); + } catch (ex) { } + }, + + /** + * Attempt to remove the tabs progress listener from the window. + * + * @param aWindow + * The window from which to remove our progress listener from + */ + _removeTabsProgressListener: function sss_removeTabsProgressListener(aWindow) { + // If there are no tabs left to restore (or restoring) in this window, then + // we can safely remove the progress listener from this window. + if (!aWindow.__SS_tabsToRestore) + try { + aWindow.getBrowser().removeTabsProgressListener(gRestoreTabsProgressListener); + } catch (ex) { } + }, + + /** + * Remove the session history listener from the tab's browser if there is one. + * + * @param aTab + * The tab who's browser to remove the listener + */ + _removeSHistoryListener: function sss_removeSHistoryListener(aTab) { + let browser = aTab.linkedBrowser; + if (browser.__SS_shistoryListener) { + browser.webNavigation.sessionHistory. + removeSHistoryListener(browser.__SS_shistoryListener); + delete browser.__SS_shistoryListener; + } + }, + +/* ........ Storage API .............. */ + + /** + * write file to disk + * @param aFile + * nsIFile + * @param aData + * String data + */ + _writeFile: function sss_writeFile(aFile, aData) { + // Initialize the file output stream. + var ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + ostream.init(aFile, 0x02 | 0x08 | 0x20, parseInt("0600", 8), ostream.DEFER_OPEN); + + // Obtain a converter to convert our data to a UTF-8 encoded input stream. + var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + // Asynchronously copy the data to the file. + var istream = converter.convertToInputStream(aData); + var ObserverService = this._observerService; + NetUtil.asyncCopy(istream, ostream, function(rc) { + if (Components.isSuccessCode(rc)) { + Services.obs.notifyObservers(null, + "sessionstore-state-write-complete"); + } + }); + } +}; + +// 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(); + } +}; + +// This is used to help meter the number of restoring tabs. This is the control +// point for telling the next tab to restore. It gets attached to each gBrowser +// via gBrowser.addTabsProgressListener +var gRestoreTabsProgressListener = { + ss: null, + onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + // Ignore state changes on browsers that we've already restored and state + // changes that aren't applicable. + if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { + // We need to reset the tab before starting the next restore. + let tab = this.ss._getTabForBrowser(aBrowser); + this.ss._resetTabRestoringState(tab); + this.ss.restoreNextTab(); + } + } +} + +// A SessionStoreSHistoryListener will be attached to each browser before it is +// restored. We need to catch reloads that occur before the tab is restored +// because otherwise, docShell will reload an old URI (usually about:blank). +function SessionStoreSHistoryListener(ss, aTab) { + this.tab = aTab; + this.ss = ss; +} + +SessionStoreSHistoryListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISHistoryListener, + Ci.nsISupportsWeakReference]), + browser: null, + ss: null, + tab: null, + OnHistoryNewEntry: function(aNewURI) { }, + OnHistoryGotoIndex: function(aIndex, aGotoURI) { }, + OnHistoryPurge: function(aNumEntries) { }, + OnHistoryReload: function(aReloadURI, aReloadFlags) { + // On reload, we want to make sure that session history loads the right + // URI. In order to do that, we will just call restoreTab. That will remove + // the history listener and load the right URI. + this.ss.restoreTab(this.tab); + // Returning false will stop the load that docshell is attempting. + return false; + }, + OnHistoryReplaceEntry: function(aIndex) { }, +} + + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStoreService]); |