diff options
Diffstat (limited to 'browser/components/sessionstore')
-rw-r--r-- | browser/components/sessionstore/DocumentUtils.jsm | 230 | ||||
-rw-r--r-- | browser/components/sessionstore/SessionStorage.jsm | 165 | ||||
-rw-r--r-- | browser/components/sessionstore/SessionStore.jsm | 4779 | ||||
-rw-r--r-- | browser/components/sessionstore/XPathGenerator.jsm | 97 | ||||
-rw-r--r-- | browser/components/sessionstore/_SessionFile.jsm | 314 | ||||
-rw-r--r-- | browser/components/sessionstore/content/aboutSessionRestore.js | 316 | ||||
-rw-r--r-- | browser/components/sessionstore/content/aboutSessionRestore.xhtml | 94 | ||||
-rw-r--r-- | browser/components/sessionstore/content/content-sessionStore.js | 40 | ||||
-rw-r--r-- | browser/components/sessionstore/jar.mn | 8 | ||||
-rw-r--r-- | browser/components/sessionstore/moz.build | 28 | ||||
-rw-r--r-- | browser/components/sessionstore/nsISessionStartup.idl | 59 | ||||
-rw-r--r-- | browser/components/sessionstore/nsISessionStore.idl | 206 | ||||
-rw-r--r-- | browser/components/sessionstore/nsSessionStartup.js | 296 | ||||
-rw-r--r-- | browser/components/sessionstore/nsSessionStore.js | 37 | ||||
-rw-r--r-- | browser/components/sessionstore/nsSessionStore.manifest | 5 |
15 files changed, 6674 insertions, 0 deletions
diff --git a/browser/components/sessionstore/DocumentUtils.jsm b/browser/components/sessionstore/DocumentUtils.jsm new file mode 100644 index 000000000..2d40a08fc --- /dev/null +++ b/browser/components/sessionstore/DocumentUtils.jsm @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = [ "DocumentUtils" ]; + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/sessionstore/XPathGenerator.jsm"); + +this.DocumentUtils = { + /** + * Obtain form data for a DOMDocument instance. + * + * The returned object has 2 keys, "id" and "xpath". Each key holds an object + * which further defines form data. + * + * The "id" object maps element IDs to values. The "xpath" object maps the + * XPath of an element to its value. + * + * @param aDocument + * DOMDocument instance to obtain form data for. + * @return object + * Form data encoded in an object. + */ + getFormData: function(aDocument) { + let formNodes = aDocument.evaluate( + XPathGenerator.restorableFormNodes, + aDocument, + XPathGenerator.resolveNS, + Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE, null + ); + + let node; + let ret = {id: {}, xpath: {}}; + + // Limit the number of XPath expressions for performance reasons. See + // bug 477564. + const MAX_TRAVERSED_XPATHS = 100; + let generatedCount = 0; + + while (node = formNodes.iterateNext()) { + 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_TRAVERSED_XPATHS) { + continue; + } + + if (node instanceof Ci.nsIDOMHTMLInputElement || + node instanceof Ci.nsIDOMHTMLTextAreaElement) { + 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 = { selectedIndex: node.selectedIndex, value: node.value }; + } else { + // <select>s with the multiple attribute are easier to determine the + // default value since each <option> has a defaultSelected + let options = Array.map(node.options, function(aOpt, aIx) { + let oSelected = aOpt.selected; + hasDefaultValue = hasDefaultValue && (oSelected == aOpt.defaultSelected); + return oSelected ? aOpt.value : -1; + }); + value = options.filter(function(aIx) aIx !== -1); + } + + // 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) { + ret.id[nId] = value; + } else { + generatedCount++; + ret.xpath[XPathGenerator.generate(node)] = value; + } + } + } + + return ret; + }, + + /** + * Merges form data on a document from previously obtained data. + * + * This is the inverse of getFormData(). The data argument is the same object + * type which is returned by getFormData(): an object containing the keys + * "id" and "xpath" which are each objects mapping element identifiers to + * form values. + * + * Where the document has existing form data for an element, the value + * will be replaced. Where the document has a form element but no matching + * data in the passed object, the element is untouched. + * + * @param aDocument + * DOMDocument instance to which to restore form data. + * @param aData + * Object defining form data. + */ + mergeFormData: function(aDocument, aData) { + if ("xpath" in aData) { + for each (let [xpath, value] in Iterator(aData.xpath)) { + let node = XPathGenerator.resolve(aDocument, xpath); + + if (node) { + this.restoreFormValue(node, value, aDocument); + } + } + } + + if ("id" in aData) { + for each (let [id, value] in Iterator(aData.id)) { + let node = aDocument.getElementById(id); + + if (node) { + this.restoreFormValue(node, value, aDocument); + } + } + } + }, + + /** + * Low-level function to restore a form value to a DOMNode. + * + * If you want a higher-level interface, see mergeFormData(). + * + * When the value is changed, the function will fire the appropriate DOM + * events. + * + * @param aNode + * DOMNode to set form value on. + * @param aValue + * Value to set form element to. + * @param aDocument [optional] + * DOMDocument node belongs to. If not defined, node.ownerDocument + * is used. + */ + restoreFormValue: function(aNode, aValue, aDocument) { + aDocument = aDocument || aNode.ownerDocument; + + let eventType; + + if (typeof aValue == "string" && aNode.type != "file") { + // Don't dispatch an input event if there is no change. + if (aNode.value == aValue) { + return; + } + + aNode.value = aValue; + eventType = "input"; + } else if (typeof aValue == "boolean") { + // Don't dispatch a change event for no change. + if (aNode.checked == aValue) { + return; + } + + aNode.checked = aValue; + eventType = "change"; + } else if (typeof aValue == "number") { + // handle select backwards compatibility, example { "#id" : index } + // We saved the value blindly since selects take more work to determine + // default values. So now we should check to avoid unnecessary events. + if (aNode.selectedIndex == aValue) { + return; + } + + if (aValue < aNode.options.length) { + aNode.selectedIndex = aValue; + eventType = "change"; + } + } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) { + // handle select new format + + // Don't dispatch a change event for no change + if (aNode.options[aNode.selectedIndex].value == aValue.value) { + return; + } + + // find first option with matching aValue if possible + for (let i = 0; i < aNode.options.length; i++) { + if (aNode.options[i].value == aValue.value) { + aNode.selectedIndex = i; + break; + } + } + eventType = "change"; + } else if (aValue && aValue.fileList && aValue.type == "file" && + aNode.type == "file") { + aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length); + eventType = "input"; + } else if (aValue && typeof aValue.indexOf == "function" && aNode.options) { + Array.forEach(aNode.options, function(opt, index) { + // don't worry about malformed options with same values + opt.selected = aValue.indexOf(opt.value) > -1; + + // Only fire the event here if this wasn't selected by default + if (!opt.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); + aNode.dispatchEvent(event); + } + } +}; diff --git a/browser/components/sessionstore/SessionStorage.jsm b/browser/components/sessionstore/SessionStorage.jsm new file mode 100644 index 000000000..8016d34bc --- /dev/null +++ b/browser/components/sessionstore/SessionStorage.jsm @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this file, +* You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["SessionStorage"]; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", + "resource:///modules/sessionstore/SessionStore.jsm"); + +this.SessionStorage = { + /** + * Updates all sessionStorage "super cookies" + * @param aDocShell + * That tab's docshell (containing the sessionStorage) + * @param aFullData + * always return privacy sensitive data (use with care) + */ + serialize: function(aDocShell, aFullData) { + return DomStorage.read(aDocShell, aFullData); + }, + + /** + * Restores all sessionStorage "super cookies". + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + * @param aStorageData + * Storage data to be restored + */ + deserialize: function(aDocShell, aStorageData) { + DomStorage.write(aDocShell, aStorageData); + } +}; + +Object.freeze(SessionStorage); + +var DomStorage = { + /** + * Reads all session storage data from the given docShell. + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + * @param aFullData + * Always return privacy sensitive data (use with care) + */ + read: function(aDocShell, aFullData) { + let data = {}; + let isPinned = aDocShell.isAppTab; + let shistory = aDocShell.sessionHistory; + + for (let i = 0; i < shistory.count; i++) { + let principal = History.getPrincipalForEntry(shistory, i, aDocShell); + if (!principal) + continue; + + // Check if we're allowed to store sessionStorage data. + let isHTTPS = principal.URI && principal.URI.schemeIs("https"); + if (aFullData || SessionStore.checkPrivacyLevel(isHTTPS, isPinned)) { + let origin = principal.extendedOrigin; + + // Don't read a host twice. + if (!(origin in data)) { + let originData = this._readEntry(principal, aDocShell); + if (Object.keys(originData).length) { + data[origin] = originData; + } + } + } + } + + return data; + }, + + /** + * Writes session storage data to the given tab. + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + * @param aStorageData + * Storage data to be restored + */ + write: function(aDocShell, aStorageData) { + for (let [host, data] in Iterator(aStorageData)) { + let uri = Services.io.newURI(host, null, null); + let principal = Services.scriptSecurityManager.getDocShellCodebasePrincipal(uri, aDocShell); + let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager); + let window = aDocShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.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. + try { + let storage = storageManager.createStorage(window, principal, "", aDocShell.usePrivateBrowsing); + } catch(e) { + Cu.reportError(e); + } + + for (let [key, value] in Iterator(data)) { + try { + storage.setItem(key, value); + } catch (e) { + // throws e.g. for URIs that can't have sessionStorage + Cu.reportError(e); + } + } + } + }, + + /** + * Reads an entry in the session storage data contained in a tab's history. + * @param aURI + * That history entry uri + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + */ + _readEntry: function(aPrincipal, aDocShell) { + let hostData = {}; + let storage; + + try { + let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager); + storage = storageManager.getStorage(aPrincipal); + } catch (e) { + // sessionStorage might throw if it's turned off, see bug 458954 + } + + if (storage && storage.length) { + for (let i = 0; i < storage.length; i++) { + try { + let key = storage.key(i); + hostData[key] = storage.getItem(key); + } catch (e) { + // This currently throws for secured items (cf. bug 442048). + } + } + } + + return hostData; + } +}; + +var History = { + /** + * Returns a given history entry's URI. + * @param aHistory + * That tab's session history + * @param aIndex + * The history entry's index + * @param aDocShell + * That tab's docshell + */ + getPrincipalForEntry: function(aHistory, + aIndex, + aDocShell) { + try { + return Services.scriptSecurityManager.getDocShellCodebasePrincipal( + aHistory.getEntryAtIndex(aIndex, false).URI, aDocShell); + } catch (e) { + // This might throw for some reason. + } + }, +}; diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm new file mode 100644 index 000000000..654f9e879 --- /dev/null +++ b/browser/components/sessionstore/SessionStore.jsm @@ -0,0 +1,4779 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["SessionStore"]; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +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"; + +// Default maximum number of tabs to restore simultaneously. Controlled by +// the browser.sessionstore.max_concurrent_tabs pref. +const DEFAULT_MAX_CONCURRENT_TAB_RESTORES = 3; + +// global notifications observed +const OBSERVING = [ + "domwindowopened", "domwindowclosed", + "quit-application-requested", "quit-application-granted", + "browser-lastwindow-close-granted", + "quit-application", "browser:purge-session-history", + "browser:purge-domain-data" +]; + +// XUL Window properties to (re)store +// Restored in restoreDimensions() +const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"]; + +// Hideable window features to (re)store +// Restored in restoreWindowFeatures() +const WINDOW_HIDEABLE_FEATURES = [ + "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars" +]; + +const MESSAGES = [ + // The content script tells us that its form data (or that of one of its + // subframes) might have changed. This can be the contents or values of + // standard form fields or of ContentEditables. + "SessionStore:input", + + // The content script has received a pageshow event. This happens when a + // page is loaded from bfcache without any network activity, i.e. when + // clicking the back or forward button. + "SessionStore:pageshow" +]; + +// These are tab events that we listen to. +const TAB_EVENTS = [ + "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned", + "TabUnpinned" +]; + +#ifndef XP_WIN +#define BROKEN_WM_Z_ORDER +#endif + +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +// debug.js adds NS_ASSERT. cf. bug 669196 +Cu.import("resource://gre/modules/debug.js", this); +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); + +XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup", + "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup"); +XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", + "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"); + +// List of docShell capabilities to (re)store. These are automatically +// retrieved from a given docShell if not already collected before. +// This is made so they're automatically in sync with all nsIDocShell.allow* +// properties. +var gDocShellCapabilities = (function() { + let caps; + + return docShell => { + if (!caps) { + let keys = Object.keys(docShell); + caps = keys.filter(k => k.startsWith("allow")).map(k => k.slice(5)); + } + + return caps; + }; +})(); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +#ifdef MOZ_DEVTOOLS +XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager", + "resource://devtools/client/scratchpad/scratchpad-manager.jsm"); + +Object.defineProperty(this, "HUDService", { + get: function() { + let devtools = Cu.import("resource://devtools/shared/Loader.jsm", {}).devtools; + return devtools.require("devtools/client/webconsole/hudservice").HUDService; + }, + configurable: true, + enumerable: true +}); +#endif + +XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils", + "resource:///modules/sessionstore/DocumentUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", + "resource:///modules/sessionstore/SessionStorage.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile", + "resource:///modules/sessionstore/_SessionFile.jsm"); + +function debug(aMsg) { + aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); + Services.console.logStringMessage(aMsg); +} + +this.SessionStore = { + get promiseInitialized() { + return SessionStoreInternal.promiseInitialized.promise; + }, + + get canRestoreLastSession() { + return SessionStoreInternal.canRestoreLastSession; + }, + + set canRestoreLastSession(val) { + SessionStoreInternal.canRestoreLastSession = val; + }, + + init: function(aWindow) { + return SessionStoreInternal.init(aWindow); + }, + + getBrowserState: function() { + return SessionStoreInternal.getBrowserState(); + }, + + setBrowserState: function(aState) { + SessionStoreInternal.setBrowserState(aState); + }, + + getWindowState: function(aWindow) { + return SessionStoreInternal.getWindowState(aWindow); + }, + + setWindowState: function(aWindow, aState, aOverwrite) { + SessionStoreInternal.setWindowState(aWindow, aState, aOverwrite); + }, + + getTabState: function(aTab) { + return SessionStoreInternal.getTabState(aTab); + }, + + setTabState: function(aTab, aState) { + SessionStoreInternal.setTabState(aTab, aState); + }, + + duplicateTab: function(aWindow, aTab, aDelta) { + return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta); + }, + + getClosedTabCount: function(aWindow) { + return SessionStoreInternal.getClosedTabCount(aWindow); + }, + + getClosedTabData: function(aWindow) { + return SessionStoreInternal.getClosedTabData(aWindow); + }, + + undoCloseTab: function(aWindow, aIndex) { + return SessionStoreInternal.undoCloseTab(aWindow, aIndex); + }, + + forgetClosedTab: function(aWindow, aIndex) { + return SessionStoreInternal.forgetClosedTab(aWindow, aIndex); + }, + + getClosedWindowCount: function() { + return SessionStoreInternal.getClosedWindowCount(); + }, + + getClosedWindowData: function() { + return SessionStoreInternal.getClosedWindowData(); + }, + + undoCloseWindow: function(aIndex) { + return SessionStoreInternal.undoCloseWindow(aIndex); + }, + + forgetClosedWindow: function(aIndex) { + return SessionStoreInternal.forgetClosedWindow(aIndex); + }, + + getWindowValue: function(aWindow, aKey) { + return SessionStoreInternal.getWindowValue(aWindow, aKey); + }, + + setWindowValue: function(aWindow, aKey, aStringValue) { + SessionStoreInternal.setWindowValue(aWindow, aKey, aStringValue); + }, + + deleteWindowValue: function(aWindow, aKey) { + SessionStoreInternal.deleteWindowValue(aWindow, aKey); + }, + + getTabValue: function(aTab, aKey) { + return SessionStoreInternal.getTabValue(aTab, aKey); + }, + + setTabValue: function(aTab, aKey, aStringValue) { + SessionStoreInternal.setTabValue(aTab, aKey, aStringValue); + }, + + deleteTabValue: function(aTab, aKey) { + SessionStoreInternal.deleteTabValue(aTab, aKey); + }, + + persistTabAttribute: function(aName) { + SessionStoreInternal.persistTabAttribute(aName); + }, + + restoreLastSession: function() { + SessionStoreInternal.restoreLastSession(); + }, + + checkPrivacyLevel: function(aIsHTTPS, aUseDefaultPref) { + return SessionStoreInternal.checkPrivacyLevel(aIsHTTPS, aUseDefaultPref); + } +}; + +// Freeze the SessionStore object. We don't want anyone to modify it. +Object.freeze(SessionStore); + +var SessionStoreInternal = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIDOMEventListener, + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]), + + // 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: {}, + + // internal states for all open windows (data we need to associate, + // but not write to disk) + _internalWindows: {}, + + // states for all recently closed windows + _closedWindows: [], + + // not-"dirty" windows usually don't need to have their data updated + _dirtyWindows: {}, + + // collection of session states yet to be restored + _statesToRestore: {}, + + // counts the number of crashes since the last clean start + _recentCrashes: 0, + + // whether the last window was closed and should be restored + _restoreLastWindow: false, + + // number of tabs currently restoring + _tabsRestoringCount: 0, + + // max number of tabs to restore concurrently + _maxConcurrentTabRestores: DEFAULT_MAX_CONCURRENT_TAB_RESTORES, + + // whether restored tabs load cached versions or force a reload + _cacheBehavior: 0, + + // 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, + + // When starting Firefox with a single private window, this is the place + // where we keep the session we actually wanted to restore in case the user + // decides to later open a non-private window as well. + _deferredInitialState: null, + + // A promise resolved once initialization is complete + _promiseInitialization: Promise.defer(), + + // Whether session has been initialized + _sessionInitialized: false, + + // True if session store is disabled by multi-process browsing. + // See bug 516755. + _disabledForMultiProcess: false, + + // The original "sessionstore.resume_session_once" preference value before it + // was modified by saveState. saveState will set the + // "sessionstore.resume_session_once" to true when the + // the "sessionstore.resume_from_crash" preference is false (crash recovery + // is disabled) so that pinned tabs will be restored in the case of a + // crash. This variable is used to restore the original value so the + // previous session is not always restored when + // "sessionstore.resume_from_crash" is true. + _resume_session_once_on_shutdown: null, + + /** + * A promise fulfilled once initialization is complete. + */ + get promiseInitialized() { + return this._promiseInitialization; + }, + + /* ........ Public Getters .............. */ + get canRestoreLastSession() { + return this._lastSessionState; + }, + + set canRestoreLastSession(val) { + this._lastSessionState = null; + }, + + /* ........ Global Event Handlers .............. */ + + /** + * Initialize the component + */ + initService: function() { + if (this._sessionInitialized) { + return; + } + OBSERVING.forEach(function(aTopic) { + Services.obs.addObserver(this, aTopic, true); + }, this); + + this._initPrefs(); + + this._disabledForMultiProcess = false; + + // this pref is only read at startup, so no need to observe it + this._sessionhistory_max_entries = + this._prefBranch.getIntPref("sessionhistory.max_entries"); + + gSessionStartup.onceInitialized.then( + this.initSession.bind(this) + ); + }, + + initSession: function() { + let ss = gSessionStartup; + try { + if (ss.doRestore() || + ss.sessionType == Ci.nsISessionStartup.DEFER_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", + formdata: { + id: { "sessionData": this._initialState }, + xpath: {} + } + }; + 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; + // We don't want to minimize and then open a window at startup. + if (this._initialState.windows[0].sizemode == "minimized") + this._initialState.windows[0].sizemode = "normal"; + // 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); } + } + + // A Lazy getter for the sessionstore.js backup promise. + XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function() { + return _SessionFile.createBackupCopy(); + }); + + // 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._initEncoding(); + + // Session is ready. + this._sessionInitialized = true; + this._promiseInitialization.resolve(); + }, + + _initEncoding : function() { + // The (UTF-8) encoder used to write to files. + XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function() { + return new TextEncoder(); + }); + }, + + _initPrefs : function() { + 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"); + }); + + 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); + + // Straight-up collect the following one-time prefs + this._maxConcurrentTabRestores = + Services.prefs.getIntPref("browser.sessionstore.max_concurrent_tabs"); + // ensure a sane value for concurrency, ignore and set default otherwise + if (this._maxConcurrentTabRestores < 1 || this._maxConcurrentTabRestores > 10) { + this._maxConcurrentTabRestores = DEFAULT_MAX_CONCURRENT_TAB_RESTORES; + } + this._cacheBehavior = + Services.prefs.getIntPref("browser.sessionstore.cache_behavior"); + + }, + + _initWindow: function(aWindow) { + 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; + } + }, + + /** + * Start tracking a window. + * + * This function also initializes the component if it is not + * initialized yet. + */ + init: function(aWindow) { + let self = this; + this.initService(); + return this._promiseInitialization.promise.then( + function onSuccess() { + self._initWindow(aWindow); + } + ); + }, + + /** + * Called on application shutdown, after notifications: + * quit-application-granted, quit-application + */ + _uninit: function() { + // save all data for session resuming + if (this._sessionInitialized) + this.saveState(true); + + // clear out priority queue in case it's still holding refs + TabRestoreQueue.reset(); + + // Make sure to break our cycle with the save timer + if (this._saveTimer) { + this._saveTimer.cancel(); + this._saveTimer = null; + } + }, + + /** + * Handle notifications + */ + observe: function(aSubject, aTopic, aData) { + if (this._disabledForMultiProcess) + return; + + switch (aTopic) { + case "domwindowopened": // catch new windows + this.onOpen(aSubject); + break; + case "domwindowclosed": // catch closed windows + this.onClose(aSubject); + break; + case "quit-application-requested": + this.onQuitApplicationRequested(); + break; + case "quit-application-granted": + this.onQuitApplicationGranted(); + break; + case "browser-lastwindow-close-granted": + this.onLastWindowCloseGranted(); + break; + case "quit-application": + this.onQuitApplication(aData); + break; + case "browser:purge-session-history": // catch sanitization + this.onPurgeSessionHistory(); + break; + case "browser:purge-domain-data": + this.onPurgeDomainData(aData); + break; + case "nsPref:changed": // catch pref changes + this.onPrefChange(aData); + break; + case "timer-callback": // timer call back for delayed saving + this.onTimerCallback(); + break; + } + }, + + /** + * This method handles incoming messages sent by the session store content + * script and thus enables communication with OOP tabs. + */ + receiveMessage: function(aMessage) { + var browser = aMessage.target; + var win = browser.ownerDocument.defaultView; + + switch (aMessage.name) { + case "SessionStore:pageshow": + this.onTabLoad(win, browser); + break; + case "SessionStore:input": + this.onTabInput(win, browser); + break; + default: + debug("received unknown message '" + aMessage.name + "'"); + break; + } + + this._clearRestoringWindows(); + }, + + /* ........ Window Event Handlers .............. */ + + /** + * Implement nsIDOMEventListener for handling various window and tab events + */ + handleEvent: function(aEvent) { + if (this._disabledForMultiProcess) + return; + + 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. + let browser = aEvent.currentTarget; + if (browser.__SS_restore_data) + this.restoreDocument(win, browser, aEvent); + this.onTabLoad(win, browser); + 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(win, aEvent.originalTarget); + break; + case "TabHide": + this.onTabHide(win, aEvent.originalTarget); + break; + case "TabPinned": + case "TabUnpinned": + this.saveStateDelayed(win); + break; + } + + this._clearRestoringWindows(); + }, + + /** + * 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(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: [], busy: false }; + + // and create its internal data object + this._internalWindows[aWindow.__SSi] = { hosts: {} } + + let isPrivateWindow = false; + if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) + this._windows[aWindow.__SSi].isPrivate = isPrivateWindow = true; + 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) { + if (isPrivateWindow) { + // We're starting with a single private window. Save the state we + // actually wanted to restore so that we can do it later in case + // the user opens another, non-private window. + this._deferredInitialState = gSessionStartup.state; + delete this._initialState; + + // Nothing to restore now, notify observers things are complete. + Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); + } else { + // 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, this._initialState)); + 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. + Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, ""); + + // 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); + } + // The user opened another, non-private window after starting up with + // a single private one. Let's restore the session we actually wanted to + // restore at startup. + else if (this._deferredInitialState && !isPrivateWindow && + aWindow.toolbar.visible) { + + this._deferredInitialState._firstTabs = true; + this._restoreCount = this._deferredInitialState.windows ? + this._deferredInitialState.windows.length : 0; + this.restoreWindow(aWindow, this._deferredInitialState, false); + this._deferredInitialState = null; + } + else if (this._restoreLastWindow && aWindow.toolbar.visible && + this._closedWindows.length && !isPrivateWindow) { + + // default to the most-recently closed window + // don't use popup windows + let closedWindowState = null; + let closedWindowIndex; + for (let i = 0; i < this._closedWindows.length; i++) { + // Take the first non-popup, point our object at it, and break out. + if (!this._closedWindows[i].isPopup) { + closedWindowState = this._closedWindows[i]; + closedWindowIndex = i; + break; + } + } + + if (closedWindowState) { + let newWindowState; + if (!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, state)); + } + } + // 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.gBrowser; + + // 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 open + * @param aWindow + * Window reference + */ + onOpen: function(aWindow) { + var _this = this; + aWindow.addEventListener("load", function(aEvent) { + aEvent.currentTarget.removeEventListener("load", arguments.callee, false); + _this.onLoad(aEvent.currentTarget); + }, false); + return; + }, + + /** + * On window close... + * - remove event listeners from tabs + * - save all window data + * @param aWindow + * Window reference + */ + onClose: function(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; + } + + // notify that the session store will stop tracking this window so that + // extensions can store any data about this window in session store before + // that's not possible anymore + let event = aWindow.document.createEvent("Events"); + event.initEvent("SSWindowClosing", true, false); + aWindow.dispatchEvent(event); + + if (this.windowToFocus && this.windowToFocus == aWindow) { + delete this.windowToFocus; + } + + var tabbrowser = aWindow.gBrowser; + + TAB_EVENTS.forEach(function(aEvent) { + tabbrowser.tabContainer.removeEventListener(aEvent, this, true); + }, this); + + // remove the progress listener for this window + tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener); + + 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); + let windows = {}; + windows[aWindow.__SSi] = winData; + this._updateCookies(windows); + } + + // Until we decide otherwise elsewhere, this window is part of a series + // of closing windows to quit. + winData._shouldRestore = true; + + // Save the window if it has multiple tabs or a single saveable tab and + // it's not private. + if (!winData.isPrivate && (winData.tabs.length > 1 || + (winData.tabs.length == 1 && this._shouldSaveTabState(winData.tabs[0])))) { + // we don't want to save the busy state + delete winData.busy; + + this._closedWindows.unshift(winData); + this._capClosedWindows(); + } + + // clear this window from the list + delete this._windows[aWindow.__SSi]; + delete this._internalWindows[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; + }, + + /** + * On quit application requested + */ + onQuitApplicationRequested: function() { + // get a current snapshot of all windows + this._forEachBrowserWindow(function(aWindow) { + this._collectWindowData(aWindow); + }); + // we must cache this because _getMostRecentBrowserWindow will always + // return null by the time quit-application occurs + var activeWindow = this._getMostRecentBrowserWindow(); + if (activeWindow) + this.activeWindowSSiCache = activeWindow.__SSi || ""; + this._dirtyWindows = []; + }, + + /** + * On quit application granted + */ + onQuitApplicationGranted: function() { + // freeze the data at what we've got (ignoring closing windows) + this._loadState = STATE_QUITTING; + }, + + /** + * On last browser window close + */ + onLastWindowCloseGranted: function() { + // last browser window is quitting. + // remember to restore the last window when another browser window is opened + // do not account for pref(resume_session_once) at this point, as it might be + // set by another observer getting this notice after us + this._restoreLastWindow = true; + }, + + /** + * On quitting application + * @param aData + * String type of quitting + */ + onQuitApplication: function(aData) { + if (aData == "restart") { + this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); + // The browser:purge-session-history notification fires after the + // quit-application notification so unregister the + // browser:purge-session-history notification to prevent clearing + // session data on disk on a restart. It is also unnecessary to + // perform any other sanitization processing on a restart as the + // browser is about to exit anyway. + Services.obs.removeObserver(this, "browser:purge-session-history"); + } + else if (this._resume_session_once_on_shutdown != null) { + // if the sessionstore.resume_session_once preference was changed by + // saveState because crash recovery is disabled then restore the + // preference back to the value it was prior to that. This will prevent + // SessionStore from always restoring the session when crash recovery is + // disabled. + this._prefBranch.setBoolPref("sessionstore.resume_session_once", + this._resume_session_once_on_shutdown); + } + + if (aData != "restart") { + // Throw away the previous session on shutdown + this._lastSessionState = null; + } + + this._loadState = STATE_QUITTING; // just to be sure + this._uninit(); + }, + + /** + * On purge of session history + */ + onPurgeSessionHistory: function() { + var _this = this; + _SessionFile.wipe(); + // If the browser is shutting down, simply return after clearing the + // session data on disk as this notification fires after the + // quit-application notification so the browser is about to exit. + if (this._loadState == STATE_QUITTING) + return; + this._lastSessionState = null; + let openWindows = {}; + this._forEachBrowserWindow(function(aWindow) { + Array.forEach(aWindow.gBrowser.tabs, function(aTab) { + delete aTab.linkedBrowser.__SS_data; + delete aTab.linkedBrowser.__SS_tabStillLoading; + delete aTab.linkedBrowser.__SS_formDataSaved; + delete aTab.linkedBrowser.__SS_hostSchemeData; + if (aTab.linkedBrowser.__SS_restoreState) + this._resetTabRestoringState(aTab); + }, this); + 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]; + delete this._internalWindows[ix]; + } + } + // also clear all data about closed windows + this._closedWindows = []; + // give the tabbrowsers a chance to clear their histories first + var win = this._getMostRecentBrowserWindow(); + if (win) + win.setTimeout(function() { _this.saveState(true); }, 0); + else if (this._loadState == STATE_RUNNING) + this.saveState(true); + // Delete the private browsing backed up state, if any + if ("_stateBackup" in this) + delete this._stateBackup; + + this._clearRestoringWindows(); + }, + + /** + * On purge of domain data + * @param aData + * String domain data + */ + onPurgeDomainData: function(aData) { + // does a session history entry contain a url for the given domain? + function containsDomain(aEntry) { + try { + if (this._getURIFromString(aEntry.url).host.hasRootDomain(aData)) + return true; + } + catch (ex) { /* url had no host at all */ } + return aEntry.children && aEntry.children.some(containsDomain, this); + } + // remove all closed tabs containing a reference to the given domain + for (let ix in this._windows) { + let closedTabs = this._windows[ix]._closedTabs; + for (let i = closedTabs.length - 1; i >= 0; i--) { + if (closedTabs[i].state.entries.some(containsDomain, this)) + closedTabs.splice(i, 1); + } + } + // remove all open & closed tabs containing a reference to the given + // domain in closed windows + for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) { + let closedTabs = this._closedWindows[ix]._closedTabs; + let openTabs = this._closedWindows[ix].tabs; + let openTabCount = openTabs.length; + for (let i = closedTabs.length - 1; i >= 0; i--) + if (closedTabs[i].state.entries.some(containsDomain, this)) + closedTabs.splice(i, 1); + for (let j = openTabs.length - 1; j >= 0; j--) { + if (openTabs[j].entries.some(containsDomain, this)) { + openTabs.splice(j, 1); + if (this._closedWindows[ix].selected > j) + this._closedWindows[ix].selected--; + } + } + if (openTabs.length == 0) { + this._closedWindows.splice(ix, 1); + } + else if (openTabs.length != openTabCount) { + // Adjust the window's title if we removed an open tab + let selectedTab = openTabs[this._closedWindows[ix].selected - 1]; + // some duplication from restoreHistory - make sure we get the correct title + let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; + if (activeIndex >= selectedTab.entries.length) + activeIndex = selectedTab.entries.length - 1; + this._closedWindows[ix].title = selectedTab.entries[activeIndex].title; + } + } + if (this._loadState == STATE_RUNNING) + this.saveState(true); + + this._clearRestoringWindows(); + }, + + /** + * On preference change + * @param aData + * String preference changed + */ + onPrefChange: function(aData) { + switch (aData) { + // if the user decreases the max number of closed tabs they want + // preserved update our internal states to match that max + case "sessionstore.max_tabs_undo": + this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo"); + for (let ix in this._windows) { + this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length); + } + break; + case "sessionstore.max_windows_undo": + this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo"); + this._capClosedWindows(); + break; + 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"); + // restore original resume_session_once preference if set in saveState + if (this._resume_session_once_on_shutdown != null) { + this._prefBranch.setBoolPref("sessionstore.resume_session_once", + this._resume_session_once_on_shutdown); + this._resume_session_once_on_shutdown = null; + } + // 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) + _SessionFile.wipe(); + this.saveState(true); + break; + } + }, + + /** + * On timer callback + */ + onTimerCallback: function() { + this._saveTimer = null; + this.saveState(); + }, + + /** + * 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(aWindow, aTab, aNoNotification) { + let browser = aTab.linkedBrowser; + browser.addEventListener("load", this, true); + + let mm = browser.messageManager; + MESSAGES.forEach(msg => mm.addMessageListener(msg, this)); + + if (!aNoNotification) { + this.saveStateDelayed(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(aWindow, aTab, aNoNotification) { + let browser = aTab.linkedBrowser; + browser.removeEventListener("load", this, true); + + let mm = browser.messageManager; + MESSAGES.forEach(msg => mm.removeMessageListener(msg, this)); + + delete browser.__SS_data; + delete browser.__SS_tabStillLoading; + delete browser.__SS_formDataSaved; + delete browser.__SS_hostSchemeData; + + // 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(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)) { + let tabTitle = aTab.label; + let tabbrowser = aWindow.gBrowser; + tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab); + + this._windows[aWindow.__SSi]._closedTabs.unshift({ + state: tabState, + title: tabTitle, + image: tabbrowser.getIcon(aTab), + pos: aTab._tPos + }); + var length = this._windows[aWindow.__SSi]._closedTabs.length; + if (length > this._max_tabs_undo) + this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo); + } + }, + + /** + * When a tab loads, save state. + * @param aWindow + * Window reference + * @param aBrowser + * Browser reference + */ + onTabLoad: function(aWindow, aBrowser) { + // 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 (aBrowser.__SS_restoreState && + aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + return; + } + + delete aBrowser.__SS_data; + delete aBrowser.__SS_tabStillLoading; + delete aBrowser.__SS_formDataSaved; + this.saveStateDelayed(aWindow); + + }, + + /** + * Called when a browser sends the "input" notification + * @param aWindow + * Window reference + * @param aBrowser + * Browser reference + */ + onTabInput: function(aWindow, aBrowser) { + // deleting __SS_formDataSaved will cause us to recollect form data + delete aBrowser.__SS_formDataSaved; + + this.saveStateDelayed(aWindow, 3000); + }, + + /** + * When a tab is selected, save session data + * @param aWindow + * Window reference + */ + onTabSelect: function(aWindow) { + if (this._loadState == STATE_RUNNING) { + this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex; + + let tab = aWindow.gBrowser.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); + + } + }, + + onTabShow: function(aWindow, aTab) { + // If the tab hasn't been restored yet, move it into the right bucket + if (aTab.linkedBrowser.__SS_restoreState && + aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + TabRestoreQueue.hiddenToVisible(aTab); + + // let's kick off tab restoration again to ensure this tab gets restored + // with "restore_hidden_tabs" == false (now that it has become visible) + this.restoreNextTab(); + } + + // Default delay of 2 seconds gives enough time to catch multiple TabShow + // events due to changing groups in Panorama. + this.saveStateDelayed(aWindow); + }, + + onTabHide: function(aWindow, aTab) { + // If the tab hasn't been restored yet, move it into the right bucket + if (aTab.linkedBrowser.__SS_restoreState && + aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { + TabRestoreQueue.visibleToHidden(aTab); + } + + // Default delay of 2 seconds gives enough time to catch multiple TabHide + // events due to changing groups in Panorama. + this.saveStateDelayed(aWindow); + }, + + /* ........ nsISessionStore API .............. */ + + getBrowserState: function() { + return this._toJSONString(this._getCurrentState()); + }, + + setBrowserState: function(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 the priority queue is emptied out + this._resetRestoringState(); + + var window = this._getMostRecentBrowserWindow(); + if (!window) { + this._restoreCount = 1; + this._openWindowWithState(state); + return; + } + + // close all other browser windows + this._forEachBrowserWindow(function(aWindow) { + if (aWindow != window) { + aWindow.close(); + this.onClose(aWindow); + } + }); + + // make sure closed window data isn't kept + this._closedWindows = []; + + // determine how many windows are meant to be restored + this._restoreCount = state.windows ? state.windows.length : 0; + + // restore to the given state + this.restoreWindow(window, state, true); + }, + + getWindowState: function(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(aWindow, aState, aOverwrite) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + this.restoreWindow(aWindow, aState, aOverwrite); + }, + + getTabState: function(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(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._setWindowStateBusy(window); + this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0); + }, + + duplicateTab: function(aWindow, aTab, aDelta) { + if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi || + !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)); + tabState.pinned = false; + + this._setWindowStateBusy(aWindow); + let newTab = aTab == aWindow.gBrowser.selectedTab ? + aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) : + aWindow.gBrowser.addTab(); + + this.restoreHistoryPrecursor(aWindow, [newTab], [tabState], 0, 0, 0, + true /* Load this tab right away. */); + + return newTab; + }, + + getClosedTabCount: function(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(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(aWindow, aIndex) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var closedTabs = this._windows[aWindow.__SSi]._closedTabs; + + // default to the most-recently closed tab + aIndex = aIndex || 0; + if (!(aIndex in closedTabs)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // fetch the data of closed tab, while removing it from the array + let closedTab = closedTabs.splice(aIndex, 1).shift(); + let closedTabState = closedTab.state; + + this._setWindowStateBusy(aWindow); + // create a new tab + let tabbrowser = aWindow.gBrowser; + let tab = tabbrowser.addTab(); + + // restore tab content + this.restoreHistoryPrecursor(aWindow, [tab], [closedTabState], 1, 0, 0); + + // restore the tab's position + tabbrowser.moveTabTo(tab, closedTab.pos); + + // focus the tab's content area (bug 342432) + tab.linkedBrowser.focus(); + + return tab; + }, + + forgetClosedTab: function(aWindow, aIndex) { + if (!aWindow.__SSi) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + var closedTabs = this._windows[aWindow.__SSi]._closedTabs; + + // default to the most-recently closed tab + aIndex = aIndex || 0; + if (!(aIndex in closedTabs)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // remove closed tab from the array + closedTabs.splice(aIndex, 1); + }, + + getClosedWindowCount: function() { + return this._closedWindows.length; + }, + + getClosedWindowData: function() { + return this._toJSONString(this._closedWindows); + }, + + undoCloseWindow: function(aIndex) { + if (!(aIndex in this._closedWindows)) + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + + // reopen the window + let state = { windows: this._closedWindows.splice(aIndex, 1) }; + let window = this._openWindowWithState(state); + this.windowToFocus = window; + return window; + }, + + forgetClosedWindow: function(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(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(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(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(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(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(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; + 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(aName) { + if (TabAttributes.persist(aName)) { + this.saveStateDelayed(); + } + }, + + /** + * 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() { + // 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._prefBranch.getIntPref("sessionstore.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(); + } + +#ifdef MOZ_DEVTOOLS + // Scratchpad + if (lastSessionState.scratchpads) { + ScratchpadManager.restoreSession(lastSessionState.scratchpads); + } + + // The Browser Console + if (lastSessionState.browserConsole) { + HUDService.restoreBrowserConsoleSession(); + } +#endif + + // 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(aWindow) { + if (!aWindow) + return [false, false]; + + let event = aWindow.document.createEvent("Events"); + event.initEvent("SSRestoreIntoWindow", true, true); + + // Check if we can use the window. + if (!aWindow.dispatchEvent(event)) + return [false, false]; + + // We might be able to overwrite the existing tabs instead of just adding + // the previous session's tabs to the end. This will be set if possible. + let canOverwriteTabs = false; + + // Look at the open tabs in comparison to home pages. If all the tabs are + // home pages then we'll end up overwriting all of them. Otherwise we'll + // just close the tabs that match home pages. Tabs with the about:blank + // URI will always be overwritten. + let homePages = ["about:blank"]; + let removableTabs = []; + let tabbrowser = aWindow.gBrowser; + let normalTabsLen = tabbrowser.tabs.length - tabbrowser._numPinnedTabs; + let startupPref = this._prefBranch.getIntPref("startup.page"); + if (startupPref == 1) + homePages = homePages.concat(aWindow.gHomeButton.getHomePage().split("|")); + + for (let i = tabbrowser._numPinnedTabs; i < tabbrowser.tabs.length; i++) { + let tab = tabbrowser.tabs[i]; + if (homePages.indexOf(tab.linkedBrowser.currentURI.spec) != -1) { + removableTabs.push(tab); + } + } + + if (tabbrowser.tabs.length == removableTabs.length) { + canOverwriteTabs = true; + } + else { + // If we're not overwriting all of the tabs, then close the home tabs. + for (let i = removableTabs.length - 1; i >= 0; i--) { + tabbrowser.removeTab(removableTabs.pop(), { animate: false }); + } + } + + return [true, canOverwriteTabs]; + }, + + /* ........ Saving Functionality .............. */ + + /** + * Store all session data for a window + * @param aWindow + * Window reference + */ + _saveWindowHistory: function(aWindow) { + var tabbrowser = aWindow.gBrowser; + 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(aTab, aFullData) { + var tabData = { entries: [], lastAccessed: aTab.lastAccessed }; + 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_tabStillLoading) { + // 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 + + // Limit number of back/forward button history entries to save + let oldest, newest; + let maxSerializeBack = this._prefBranch.getIntPref("sessionstore.max_serialize_back"); + if (maxSerializeBack >= 0) { + oldest = Math.max(0, history.index - maxSerializeBack); + } else { // History.getEntryAtIndex(0, ...) is the oldest. + oldest = 0; + } + let maxSerializeFwd = this._prefBranch.getIntPref("sessionstore.max_serialize_forward"); + if (maxSerializeFwd >= 0) { + newest = Math.min(history.count - 1, history.index + maxSerializeFwd); + } else { // History.getEntryAtIndex(history.count - 1, ...) is the newest. + newest = history.count - 1; + } + + // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse + // data even when we shouldn't (e.g. Back, different anchor) + // Warning: this is required to save form data and scrolling position! + 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) { + try { + tabData.entries = browser.__SS_data.entries.slice(oldest, newest + 1); + } + catch (ex) { + // No errors are expected above, but we use try-catch to keep sessionstore.js safe + NS_ASSERT(false, "SessionStore failed to slice history from browser.__SS_data"); + } + + // Set the one-based index of the currently active tab, ensuring it isn't out of bounds + tabData.index = Math.min(history.index - oldest + 1, tabData.entries.length); + } + else if (history && history.count > 0) { + browser.__SS_hostSchemeData = []; + try { + for (var j = oldest; j <= newest; j++) { + let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false), + aFullData, aTab.pinned, browser.__SS_hostSchemeData); + tabData.entries.push(entry); + } + } + 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. + NS_ASSERT(false, "SessionStore failed gathering complete history " + + "for the focused window/tab. See bug 669196."); + } + + // Set the one-based index of the currently active tab, ensuring it isn't out of bounds + tabData.index = Math.min(history.index - oldest + 1, tabData.entries.length); + + // 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 }; + 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; + // We always used to keep track of the loading state as an integer, where + // '0' indicated the user had typed since the last load (or no load was + // ongoing), and any positive value indicated we had started a load since + // the last time the user typed in the URL bar. Mimic this to keep the + // session store representation in sync, even though we now represent this + // more explicitly: + tabData.userTypedClear = browser.didStartLoadSinceLastUserTyping() ? 1 : 0; + } else { + delete tabData.userTypedValue; + delete tabData.userTypedClear; + } + + if (aTab.pinned) + tabData.pinned = true; + else + delete tabData.pinned; + tabData.hidden = aTab.hidden; + + var disallow = []; + for (let cap of gDocShellCapabilities(browser.docShell)) + if (!browser.docShell["allow" + cap]) + disallow.push(cap); + if (disallow.length > 0) + tabData.disallow = disallow.join(","); + else if (tabData.disallow) + delete tabData.disallow; + + // Save tab attributes. + tabData.attributes = TabAttributes.get(aTab); + + // Store the tab icon. + let tabbrowser = aTab.ownerDocument.defaultView.gBrowser; + tabData.image = tabbrowser.getIcon(aTab); + + if (aTab.__SS_extdata) + tabData.extData = aTab.__SS_extdata; + else if (tabData.extData) + delete tabData.extData; + + if (history && browser.docShell instanceof Ci.nsIDocShell) { + let storageData = SessionStorage.serialize(browser.docShell, aFullData) + if (Object.keys(storageData).length) + tabData.storage = storageData; + } + + 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 + * @param aHostSchemeData + * an array of objects with host & scheme keys + * @returns object + */ + _serializeHistoryEntry: + function(aEntry, aFullData, aIsPinned, aHostSchemeData) { + var entry = { url: aEntry.URI.spec }; + + try { + // throwing is expensive, we know that about: pages will throw + if (entry.url.indexOf("about:") != 0) + aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme }); + } + catch (ex) { + // We just won't attempt to get cookies for this entry. + } + + 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.docshellID = aEntry.docshellID; + + if (aEntry.referrerURI) + entry.referrer = aEntry.referrerURI.spec; + + if (aEntry.srcdocData) + entry.srcdocData = aEntry.srcdocData; + + if (aEntry.isSrcdocEntry) + entry.isSrcdocEntry = aEntry.isSrcdocEntry; + + 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 + + if (aEntry.triggeringPrincipal) { + // Not catching anything specific here, just possible errors + // from writeCompoundObject and the like. + try { + var binaryStream = Cc["@mozilla.org/binaryoutputstream;1"]. + createInstance(Ci.nsIObjectOutputStream); + var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + binaryStream.setOutputStream(pipe.outputStream); + binaryStream.writeCompoundObject(aEntry.triggeringPrincipal, Ci.nsIPrincipal, true); + binaryStream.close(); + + // Now we want to read the data from the pipe's input end and encode it. + var scriptableStream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + scriptableStream.setInputStream(pipe.inputStream); + var triggeringPrincipalBytes = + scriptableStream.readByteArray(scriptableStream.available()); + // We can stop doing base64 encoding once our serialization into JSON + // is guaranteed to handle all chars in strings, including embedded + // nulls. + entry.triggeringPrincipal_b64 = btoa(String.fromCharCode.apply(null, triggeringPrincipalBytes)); + } + catch (ex) { debug(ex); } + } + + entry.docIdentifier = aEntry.BFCacheEntry.ID; + + if (aEntry.stateData != null) { + entry.structuredCloneState = aEntry.stateData.getDataAsBase64(); + entry.structuredCloneVersion = aEntry.stateData.formatVersion; + } + + if (!(aEntry instanceof Ci.nsISHContainer)) { + return entry; + } + + if (aEntry.childCount > 0) { + let children = []; + for (var i = 0; i < aEntry.childCount; i++) { + var child = aEntry.GetChildAt(i); + + if (child) { + // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) + if (child.URI.schemeIs("wyciwyg")) { + children = []; + break; + } + + children.push(this._serializeHistoryEntry(child, aFullData, + aIsPinned, aHostSchemeData)); + } + } + + if (children.length) + entry.children = children; + } + + return entry; + }, + + /** + * go through all tabs and store the current scroll positions + * and innerHTML content of WYSIWYG editors + * @param aWindow + * Window reference + */ + _updateTextAndScrollData: function(aWindow) { + var browsers = aWindow.gBrowser.browsers; + this._windows[aWindow.__SSi].tabs.forEach(function(tabData, i) { + try { + this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData); + } + catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time) + }, this); + }, + + /** + * 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(aWindow, aBrowser, aTabData, aFullData) { + // we shouldn't update data for incompletely initialized tabs + if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading) + return; + + 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], + !aBrowser.__SS_formDataSaved, aFullData, + !!aTabData.pinned); + aBrowser.__SS_formDataSaved = true; + if (aBrowser.currentURI.spec == "about:config") + aTabData.entries[tabIndex].formdata = { + id: { + "textbox": aBrowser.contentDocument.getElementById("textbox").value + }, + xpath: {} + }; + }, + + /** + * 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 aUpdateFormData + * update all form data for this tab + * @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(aWindow, aContent, aData, + aUpdateFormData, 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], aUpdateFormData, + aFullData, aIsPinned); + } + var isHTTPS = this._getURIFromString((aContent.parent || aContent). + document.location.href).schemeIs("https"); + let isAboutSR = aContent.top.document.location.href == "about:sessionrestore"; + if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) { + if (aFullData || aUpdateFormData) { + let formData = DocumentUtils.getFormData(aContent.document); + + // We want to avoid saving data for about:sessionrestore as a string. + // Since it's stored in the form as stringified JSON, stringifying further + // causes an explosion of escape characters. cf. bug 467409 + if (formData && isAboutSR) { + formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]); + } + + if (Object.keys(formData.id).length || + Object.keys(formData.xpath).length) { + 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" && aContent.document.body) + 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(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 ""; + }, + + /** + * 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(aEntry, aHosts, aCheckPrivacy, aIsPinned) { + + let host = aEntry._host, + scheme = aEntry._scheme; + + // If host & scheme aren't defined, then we are likely here in the startup + // process via _splitCookiesFromWindow. In that case, we'll turn aEntry.url + // into an nsIURI and get host/scheme from that. This will throw for about: + // urls in which case we don't need to do anything. + if (!host && !scheme) { + try { + let uri = this._getURIFromString(aEntry.url); + host = uri.host; + scheme = uri.scheme; + this._extractHostsForCookiesFromHostScheme(host, scheme, aHosts, aCheckPrivacy, aIsPinned); + } + catch(ex) { } + } + + 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(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; + } + }, + + /** + * store all hosts for a URL + * @param aWindow + * Window reference + */ + _updateCookieHosts: function(aWindow) { + var hosts = this._internalWindows[aWindow.__SSi].hosts = {}; + + // Since _updateCookiesHosts is only ever called for open windows during a + // session, we can call into _extractHostsForCookiesFromHostScheme directly + // using data that is attached to each browser. + for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) { + let tab = aWindow.gBrowser.tabs[i]; + let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || []; + for (let j = 0; j < hostSchemeData.length; j++) { + this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host, + hostSchemeData[j].scheme, + hosts, true, tab.pinned); + } + } + }, + + /** + * Serialize cookie data + * @param aWindows + * JS object containing window data references + * { id: winData, etc. } + */ + _updateCookies: function(aWindows) { + function addCookieToHash(aHash, aHost, aPath, aName, aCookie) { + // lazily build up a 3-dimensional hash, with + // aHost, aPath, and aName as keys + if (!aHash[aHost]) + aHash[aHost] = {}; + if (!aHash[aHost][aPath]) + aHash[aHost][aPath] = {}; + aHash[aHost][aPath][aName] = aCookie; + } + + var jscookies = {}; + var _this = this; + // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision + var MAX_EXPIRY = Math.pow(2, 62); + + for (let [id, window] in Iterator(aWindows)) { + window.cookies = []; + let internalWindow = this._internalWindows[id]; + if (!internalWindow.hosts) + return; + for (var [host, isPinned] in Iterator(internalWindow.hosts)) { + let list; + try { + list = Services.cookies.getCookiesFromHost(host, {}); + } + catch (ex) { + debug("getCookiesFromHost failed. Host: " + host); + } + while (list && 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 + if (!(cookie.host in jscookies && + cookie.path in jscookies[cookie.host] && + cookie.name in jscookies[cookie.host][cookie.path])) { + 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; + + addCookieToHash(jscookies, cookie.host, cookie.path, cookie.name, jscookie); + } + window.cookies.push(jscookies[cookie.host][cookie.path][cookie.name]); + } + } + } + + // don't include empty cookie sections + if (!window.cookies.length) + delete window.cookies; + } + }, + + /** + * Store window dimensions, visibility, sidebar + * @param aWindow + * Window reference + */ + _updateWindowFeatures: function(aWindow) { + var winData = this._windows[aWindow.__SSi]; + + WINDOW_ATTRIBUTES.forEach(function(aAttr) { + winData[aAttr] = this._getWindowDimension(aWindow, aAttr); + }, this); + + var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) { + return aWindow[aItem] && !aWindow[aItem].visible; + }); + if (hidden.length != 0) + winData.hidden = hidden.join(","); + else if (winData.hidden) + delete winData.hidden; + + var sidebar = aWindow.document.getElementById("sidebar-box").getAttribute("sidebarcommand"); + if (sidebar) + winData.sidebar = sidebar; + else if (winData.sidebar) + delete winData.sidebar; + }, + + /** + * gather session data as object + * @param aUpdateAll + * Bool update all windows + * @param aPinnedOnly + * Bool collect pinned tabs only + * @returns object + */ + _getCurrentState: function(aUpdateAll, aPinnedOnly) { + 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 || this._dirtyWindows[aWindow.__SSi] || aWindow == activeWindow) { + this._collectWindowData(aWindow); + } + else { // always update the window features (whose change alone never triggers a save operation) + this._updateWindowFeatures(aWindow); + } + }); + this._dirtyWindows = []; + } + + // collect the data for all windows + var total = [], windows = {}, 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); + windows[ix] = this._windows[ix]; + if (!this._windows[ix].isPopup) + nonPopupCount++; + } + this._updateCookies(windows); + + // collect the data for all windows yet to be restored + for (ix in this._statesToRestore) { + for each (let winData in 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 (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 && lastClosedWindowsCopy.length > 0) + } + + if (aPinnedOnly) { + // perform a deep copy so that existing session variables are not changed. + total = JSON.parse(this._toJSONString(total)); + total = total.filter(function(win) { + win.tabs = win.tabs.filter(function(tab) tab.pinned); + // remove closed tabs + win._closedTabs = []; + // correct selected tab index if it was stripped out + if (win.selected > win.tabs.length) + win.selected = 1; + return win.tabs.length > 0; + }); + if (total.length == 0) + return null; + + lastClosedWindowsCopy = []; + } + + if (activeWindow) { + this.activeWindowSSiCache = activeWindow.__SSi || ""; + } + ix = ids.indexOf(this.activeWindowSSiCache); + // We don't want to restore focus to a minimized window or a window which had all its + // tabs stripped out (doesn't exist). + if (ix != -1 && total[ix] && total[ix].sizemode == "minimized") + ix = -1; + + let session = { + state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR, + lastUpdate: Date.now(), + startTime: this._sessionStartTime, + recentCrashes: this._recentCrashes + }; + + var scratchpads = null; + var browserConsole = null; +#ifdef MOZ_DEVTOOLS + // Scratchpad + // get open Scratchpad window states too + scratchpads = ScratchpadManager.getSessionState(); + + // The Browser Console + browserConsole = HUDService.getBrowserConsoleSessionState(); +#endif + + return { + windows: total, + selectedWindow: ix + 1, + _closedWindows: lastClosedWindowsCopy, +#ifdef MOZ_DEVTOOLS + session: session, + scratchpads: scratchpads, + browserConsole: browserConsole +#else + session: session +#endif + }; + }, + + /** + * serialize session data for a window + * @param aWindow + * Window reference + * @returns string + */ + _getWindowState: function(aWindow) { + if (!this._isWindowLoaded(aWindow)) + return this._statesToRestore[aWindow.__SS_restoreID]; + + if (this._loadState == STATE_RUNNING) { + this._collectWindowData(aWindow); + } + + var winData = this._windows[aWindow.__SSi]; + let windows = {}; + windows[aWindow.__SSi] = winData; + this._updateCookies(windows); + + return { windows: [winData] }; + }, + + _collectWindowData: function(aWindow) { + if (!this._isWindowLoaded(aWindow)) + return; + + // update the internal state data for this window + this._saveWindowHistory(aWindow); + this._updateTextAndScrollData(aWindow); + this._updateCookieHosts(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; + + this._dirtyWindows[aWindow.__SSi] = false; + }, + + /* ........ 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(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._setWindowStateBusy(aWindow); + + if (root._closedWindows) + this._closedWindows = root._closedWindows; + + var winData; + if (!root.selectedWindow || root.selectedWindow > root.windows.length) { + root.selectedWindow = 0; + } + + // open new windows for all further window entries of a multi-window session + // (unless they don't contain any tab data) + for (var w = 1; w < root.windows.length; w++) { + winData = root.windows[w]; + if (winData && winData.tabs && winData.tabs[0]) { + var window = this._openWindowWithState({ windows: [winData] }); + if (w == root.selectedWindow - 1) { + this.windowToFocus = window; + } + } + } + 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.gBrowser; + 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.mTabstrip; + var smoothScroll = tabstrip.smoothScroll; + tabstrip.smoothScroll = false; + + // unpin all tabs to ensure they are not reordered in the next loop + if (aOverwriteTabs) { + for (let t = tabbrowser._numPinnedTabs - 1; t > -1; t--) + tabbrowser.unpinTab(tabbrowser.tabs[t]); + } + + // make sure that the selected tab won't be closed in order to + // prevent unnecessary flickering + if (aOverwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount) + tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1); + + let numVisibleTabs = 0; + + for (var t = 0; t < newTabCount; t++) { + tabs.push(t < openTabCount ? + tabbrowser.tabs[t] : + tabbrowser.addTab("about:blank", + {skipAnimation: true, + skipBackgroundNotify: true})); + // when resuming at startup: add additionally requested pages to the end + if (!aOverwriteTabs && root._firstTabs) { + tabbrowser.moveTabTo(tabs[t], t); + } + + if (winData.tabs[t].pinned) + tabbrowser.pinTab(tabs[t]); + + if (winData.tabs[t].hidden) { + tabbrowser.hideTab(tabs[t]); + } + else { + tabbrowser.showTab(tabs[t]); + numVisibleTabs++; + } + } + + // if all tabs to be restored are hidden, make the first one visible + if (!numVisibleTabs && winData.tabs.length) { + winData.tabs[0].hidden = false; + tabbrowser.showTab(tabs[0]); + } + + // If overwriting tabs, we want to reset each tab's "restoring" state. Since + // we're overwriting those tabs, they should no longer be restoring. The + // tabs will be rebuilt and marked if they need to be restored after loading + // state (in 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 + if (aOverwriteTabs && newTabCount < openTabCount) { + Array.slice(tabbrowser.tabs, newTabCount, openTabCount) + .forEach(tabbrowser.removeTab, tabbrowser); + } + + 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); + +#ifdef MOZ_DEVTOOLS + if (aState.scratchpads) { + ScratchpadManager.restoreSession(aState.scratchpads); + } + + // The Browser Console + if (aState.browserConsole) { + HUDService.restoreBrowserConsoleSession(); + } + +#endif + // set smoothScroll back to the original value + tabstrip.smoothScroll = smoothScroll; + + this._sendRestoreCompletedNotifications(); + }, + + /** + * Sets the tabs restoring order with the following priority: + * Selected tab, pinned tabs, optimized visible tabs, other visible tabs and + * hidden tabs. + * @param aTabBrowser + * Tab browser object + * @param aTabs + * Array of tab references + * @param aTabData + * Array of tab data + * @param aSelectedTab + * Index of selected tab (1 is first tab, 0 no selected tab) + */ + _setTabsRestoringOrder : function( + aTabBrowser, aTabs, aTabData, aSelectedTab) { + + // Store the selected tab. Need to substract one to get the index in aTabs. + let selectedTab; + if (aSelectedTab > 0 && aTabs[aSelectedTab - 1]) { + selectedTab = aTabs[aSelectedTab - 1]; + } + + // Store the pinned tabs and hidden tabs. + let pinnedTabs = []; + let pinnedTabsData = []; + let hiddenTabs = []; + let hiddenTabsData = []; + if (aTabs.length > 1) { + for (let t = aTabs.length - 1; t >= 0; t--) { + if (aTabData[t].pinned) { + pinnedTabs.unshift(aTabs.splice(t, 1)[0]); + pinnedTabsData.unshift(aTabData.splice(t, 1)[0]); + } else if (aTabData[t].hidden) { + hiddenTabs.unshift(aTabs.splice(t, 1)[0]); + hiddenTabsData.unshift(aTabData.splice(t, 1)[0]); + } + } + } + + // Optimize the visible tabs only if there is a selected tab. + if (selectedTab) { + let selectedTabIndex = aTabs.indexOf(selectedTab); + if (selectedTabIndex > 0) { + let scrollSize = aTabBrowser.tabContainer.mTabstrip.scrollClientSize; + let tabWidth = aTabs[0].getBoundingClientRect().width; + let maxVisibleTabs = Math.ceil(scrollSize / tabWidth); + if (maxVisibleTabs < aTabs.length) { + let firstVisibleTab = 0; + let nonVisibleTabsCount = aTabs.length - maxVisibleTabs; + if (nonVisibleTabsCount >= selectedTabIndex) { + // Selected tab is leftmost since we scroll to it when possible. + firstVisibleTab = selectedTabIndex; + } else { + // Selected tab is rightmost or no more room to scroll right. + firstVisibleTab = nonVisibleTabsCount; + } + aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs); + aTabData = + aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData); + } + } + } + + // Merge the stored tabs in order. + aTabs = pinnedTabs.concat(aTabs, hiddenTabs); + aTabData = pinnedTabsData.concat(aTabData, hiddenTabsData); + + // Load the selected tab to the first position and select it. + if (selectedTab) { + let selectedTabIndex = aTabs.indexOf(selectedTab); + if (selectedTabIndex > 0) { + aTabs = aTabs.splice(selectedTabIndex, 1).concat(aTabs); + aTabData = aTabData.splice(selectedTabIndex, 1).concat(aTabData); + } + aTabBrowser.selectedTab = selectedTab; + } + + return [aTabs, aTabData]; + }, + + /** + * 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 + * @param aRestoreImmediately + * Flag to indicate whether the given set of tabs aTabs should be + * restored/loaded immediately even if restore_on_demand = true + */ + restoreHistoryPrecursor: + function(aWindow, aTabs, aTabData, aSelectTab, + aIx, aCount, aRestoreImmediately = false) { + var tabbrowser = aWindow.gBrowser; + + // 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, aRestoreImmediately); + } + 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. + this._dirtyWindows[aWindow.__SSi] = true; + } + + 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._setWindowStateReady(aWindow); + return; + } + + // Sets the tabs restoring order. + [aTabs, aTabData] = + this._setTabsRestoringOrder(tabbrowser, aTabs, aTabData, aSelectTab); + + // 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.pinned) + tabbrowser.pinTab(tab); + else + tabbrowser.unpinTab(tab); + + if (tabData.hidden) + tabbrowser.hideTab(tab); + else + tabbrowser.showTab(tab); + + if ("attributes" in tabData) { + // Ensure that we persist tab attributes restored from previous sessions. + Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a)); + } + + browser.__SS_tabStillLoading = 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; + browser.setAttribute("pending", "true"); + tab.setAttribute("pending", "true"); + + // 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; + 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, + aRestoreImmediately); + }, + + /** + * 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 + * @param aRestoreImmediately + * Flag to indicate whether the given set of tabs aTabs should be + * restored/loaded immediately even if restore_on_demand = true + */ + restoreHistory: + function(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap, + aRestoreImmediately) { + var _this = this; + // if the tab got removed before being completely restored, then skip it + while (aTabs.length > 0 && !(this._canRestoreTabHistory(aTabs[0]))) { + 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._setWindowStateReady(aWindow); + return; // no more tabs to restore + } + + var tab = aTabs.shift(); + var tabData = aTabData.shift(); + + var browser = aWindow.gBrowser.getBrowserForTab(tab); + var history = browser.webNavigation.sessionHistory; + + if (history.count > 0) { + history.PurgeHistory(history.count); + } + history.QueryInterface(Ci.nsISHistoryInternal); + + browser.__SS_shistoryListener = new SessionStoreSHistoryListener(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++) { + //XXXzpao Wallpaper patch for bug 514751 + if (!tabData.entries[i].url) + continue; + history.addEntry(this._deserializeHistoryEntry(tabData.entries[i], + aIdMap, aDocIdentMap), true); + } + + // make sure to reset the capabilities and attributes, in case this tab gets reused + let disallow = new Set(tabData.disallow && tabData.disallow.split(",")); + for (let cap of gDocShellCapabilities(browser.docShell)) + browser.docShell["allow" + cap] = !disallow.has(cap); + + // Restore tab attributes. + if ("attributes" in tabData) { + TabAttributes.set(tab, tabData.attributes); + } + + // Restore the tab icon. + if ("image" in tabData) { + // Using null as the loadingPrincipal because serializing + // the principal would be overkill. Within SetIcon we + // default to the systemPrincipal if aLoadingPrincipal is + // null which will allow the favicon to load. + aWindow.gBrowser.setIcon(tab, tabData.image, null); + } + + if (tabData.storage && browser.docShell instanceof Ci.nsIDocShell) + SessionStorage.deserialize(browser.docShell, tabData.storage); + + // 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 + aWindow.setTimeout(function(){ + _this.restoreHistory(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap, + aRestoreImmediately); + }, 0); + + // This could cause us to ignore max_concurrent_tabs pref a bit, but + // it ensures each window will have its selected tab loaded. + if (aRestoreImmediately || aWindow.gBrowser.selectedBrowser == browser) { + this.restoreTab(tab); + } + else { + TabRestoreQueue.add(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(aTab) { + let window = aTab.ownerDocument.defaultView; + let browser = aTab.linkedBrowser; + let tabData = browser.__SS_data; + + // 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 the priority queue. + TabRestoreQueue.remove(aTab); + + // Increase our internal count. + this._tabsRestoringCount++; + + // Set this tab's state to restoring + browser.__SS_restoreState = TAB_STATE_RESTORING; + browser.removeAttribute("pending"); + aTab.removeAttribute("pending"); + + // 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. This creates a new session history entry with a new + // doc identifier, so we need to explicitly save and restore the old doc + // identifier (corresponding to the SHEntry at activeIndex) below. + 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. + browser.webNavigation.sessionHistory.getEntryAtIndex(activeIndex, true); + browser.webNavigation.sessionHistory.reloadCurrentEntry(); + // If the user prefers it, bypass cache and always load from the network, + // but only if restoring on demand, to prevent request flooding (since + // reloading will override the max tabs to restore concurrently mechanism). + // See Issue #1772 + if (TabRestoreQueue.prefs.restoreOnDemand) { + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + switch (this._cacheBehavior) { + case 2: // hard refresh + flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + browser.webNavigation.reload(flags); + break; + case 1: // soft refresh + browser.webNavigation.reload(flags); + break; + default: // 0 or other: use cache, so do nothing. + break; + } + } + } + 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; + if (didStartLoad) + browser.stop(); + didStartLoad = true; + browser.loadURIWithFlags(tabData.userTypedValue, + Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP); + } + } + + // 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() { + // If we call in here while quitting, we don't actually want to do anything + if (this._loadState == STATE_QUITTING) + return; + + // Don't exceed the maximum number of concurrent tab restores. + if (this._tabsRestoringCount >= this._maxConcurrentTabRestores) + return; + + let tab = TabRestoreQueue.shift(); + if (tab) { + 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(aEntry, aIdMap, aDocIdentMap) { + + var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. + createInstance(Ci.nsISHEntry); + + shEntry.setURI(this._getURIFromString(aEntry.url)); + shEntry.setTitle(aEntry.title || aEntry.url); + if (aEntry.subframe) + shEntry.setIsSubFrame(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.isSrcdocEntry) + shEntry.srcdocData = aEntry.srcdocData; + + 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 (aEntry.docshellID) + shEntry.docshellID = aEntry.docshellID; + + 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 aEntry.owner_b64 got renamed to aEntry.triggeringPricipal_b64 in + // Bug 1286472. To remain backward compatible we still have to support that + // field for a few cycles before we can remove it within Bug 1289785. + if (aEntry.owner_b64) { + aEntry.triggeringPrincipal_b64 = aEntry.owner_b64; + delete aEntry.owner_b64; + } + + if (aEntry.triggeringPrincipal_b64) { + var triggeringPrincipalInput = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + var binaryData = atob(aEntry.triggeringPrincipal_b64); + triggeringPrincipalInput.setData(binaryData, binaryData.length); + var binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIObjectInputStream); + binaryStream.setInputStream(triggeringPrincipalInput); + try { // Catch possible deserialization exceptions + shEntry.triggeringPrincipal = binaryStream.readObject(true); + } catch (ex) { debug(ex); } + } + + if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { + for (var i = 0; i < aEntry.children.length; i++) { + //XXXzpao Wallpaper patch for bug 514751 + if (!aEntry.children[i].url) + continue; + + // We're getting sessionrestore.js files with a cycle in the + // doc-identifier graph, likely due to bug 698656. (That is, we have + // an entry where doc identifier A is an ancestor of doc identifier B, + // and another entry where doc identifier B is an ancestor of A.) + // + // If we were to respect these doc identifiers, we'd create a cycle in + // the SHEntries themselves, which causes the docshell to loop forever + // when it looks for the root SHEntry. + // + // So as a hack to fix this, we restrict the scope of a doc identifier + // to be a node's siblings and cousins, and pass childDocIdents, not + // aDocIdents, to _deserializeHistoryEntry. That is, we say that two + // SHEntries with the same doc identifier have the same document iff + // they have the same parent or their parents have the same document. + + shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, + childDocIdents), i); + } + } + + return shEntry; + }, + + /** + * Restore properties to a loaded document + */ + restoreDocument: function(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) + !aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, ""); + + let selectedPageStyle = aBrowser.__SS_restore_pageStyle; + function restoreTextDataAndScrolling(aContent, aData, aPrefix) { + if (aData.formdata && hasExpectedURL(aContent.document, aData.url)) { + let formdata = aData.formdata; + + // handle backwards compatibility + // this is a migration from pre-firefox 15. cf. bug 742051 + if (!("xpath" in formdata || "id" in formdata)) { + formdata = { xpath: {}, id: {} }; + + for each (let [key, value] in Iterator(aData.formdata)) { + if (key.charAt(0) == "#") { + formdata.id[key.slice(1)] = value; + } else { + formdata.xpath[key] = value; + } + } + } + + // for about:sessionrestore we saved the field as JSON to avoid + // nested instances causing humongous sessionstore.js files. + // cf. bug 467409 + if (aData.url == "about:sessionrestore" && + "sessionData" in formdata.id && + typeof formdata.id["sessionData"] == "object") { + formdata.id["sessionData"] = + JSON.stringify(formdata.id["sessionData"]); + } + + // update the formdata + aData.formdata = formdata; + // merge the formdata + DocumentUtils.mergeFormData(aContent.document, formdata); + } + + if (aData.innerHTML) { + aWindow.setTimeout(function() { + if (aContent.document.designMode == "on" && + hasExpectedURL(aContent.document, aData.url) && + aContent.document.body) { + 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.forEach(aContent.document.styleSheets, 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(aWindow, aWinData) { + var hidden = (aWinData.hidden)?aWinData.hidden.split(","):[]; + WINDOW_HIDEABLE_FEATURES.forEach(function(aItem) { + aWindow[aItem].visible = hidden.indexOf(aItem) == -1; + }); + + if (aWinData.isPopup) { + this._windows[aWindow.__SSi].isPopup = true; + if (aWindow.gURLBar) { + aWindow.gURLBar.readOnly = true; + aWindow.gURLBar.setAttribute("enablehistory", "false"); + } + } + else { + delete this._windows[aWindow.__SSi].isPopup; + if (aWindow.gURLBar) { + aWindow.gURLBar.readOnly = false; + aWindow.gURLBar.setAttribute("enablehistory", "true"); + } + } + + var _this = this; + aWindow.setTimeout(function() { + _this.restoreDimensions.apply(_this, [aWindow, + +aWinData.width || 0, + +aWinData.height || 0, + "screenX" in aWinData ? +aWinData.screenX : NaN, + "screenY" in aWinData ? +aWinData.screenY : NaN, + aWinData.sizemode || "", aWinData.sidebar || ""]); + }, 0); + }, + + /** + * Restore a window's dimensions + * @param aWidth + * Window width + * @param aHeight + * Window height + * @param aLeft + * Window left + * @param aTop + * Window top + * @param aSizeMode + * Window size mode (eg: maximized) + * @param aSidebar + * Sidebar command + */ + restoreDimensions: function(aWindow, aWidth, aHeight, aLeft, aTop, aSizeMode, aSidebar) { + var win = aWindow; + var _this = this; + function win_(aName) { return _this._getWindowDimension(win, aName); } + + // Find available space on the screen where this window is being placed + let screen = gScreenManager.screenForRect(aLeft, aTop, aWidth, aHeight); + if (screen && !this._prefBranch.getBoolPref("sessionstore.exactPos")) { + let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {}; + screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight); + + // Screen X/Y are based on the origin of the screen's desktop-pixel coordinate space + let screenLeftCss = screenLeft.value; + let screenTopCss = screenTop.value; + + // Convert the screen's device pixel dimensions to CSS px dimensions + screen.GetAvailRect(screenLeft, screenTop, screenWidth, screenHeight); + let cssToDevScale = screen.defaultCSSScaleFactor; + let screenRightCss = screenLeftCss + screenWidth.value / cssToDevScale; + let screenBottomCss = screenTopCss + screenHeight.value / cssToDevScale; + + // Pull the window within the screen's bounds. + // First, ensure the left edge is on-screen + if (aLeft < screenLeftCss) { + aLeft = screenLeftCss; + } + // Then check the resulting right edge, and reduce it if necessary. + let right = aLeft + aWidth; + if (right > screenRightCss) { + right = screenRightCss; + // See if we can move the left edge leftwards to maintain width. + if (aLeft > screenLeftCss) { + aLeft = Math.max(right - aWidth, screenLeftCss); + } + } + // Finally, update aWidth to account for the adjusted left and right edges. + aWidth = right - aLeft; + + // Do the same in the vertical dimension. + // First, ensure the top edge is on-screen + if (aTop < screenTopCss) { + aTop = screenTopCss; + } + // Then check the resulting right edge, and reduce it if necessary. + let bottom = aTop + aHeight; + if (bottom > screenBottomCss) { + bottom = screenBottomCss; + // See if we can move the top edge upwards to maintain height. + if (aTop > screenTopCss) { + aTop = Math.max(bottom - aHeight, screenTopCss); + } + } + // Finally, update aHeight to account for the adjusted top and bottom edges. + aHeight = bottom - aTop; + } + + // Only modify those aspects which aren't correct yet + if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) { + aWindow.moveTo(aLeft, aTop); + } + if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) { + // Don't resize the window if it's currently maximized and we would + // maximize it again shortly after. + if (aSizeMode != "maximized" || win_("sizemode") != "maximized") { + aWindow.resizeTo(aWidth, aHeight); + } + } + + // Restore window state + 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.focus(); + } + }, + + /** + * Restores cookies + * @param aCookies + * Array of cookie objects + */ + restoreCookies: function(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, {}); + } + 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(aWindow, aDelay) { + if (aWindow) { + this._dirtyWindows[aWindow.__SSi] = true; + } + + if (!this._saveTimer) { + // 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(aUpdateAll) { + // 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, pinnedOnly); + if (!oState) { + return; + } + + // Forget about private windows. + for (let i = oState.windows.length - 1; i >= 0; i--) { + if (oState.windows[i].isPrivate) { + oState.windows.splice(i, 1); + if (oState.selectedWindow >= i) { + oState.selectedWindow--; + } + } + } + + for (let i = oState._closedWindows.length - 1; i >= 0; i--) { + if (oState._closedWindows[i].isPrivate) { + oState._closedWindows.splice(i, 1); + } + } + + // We want to restore closed windows that are marked with _shouldRestore. + // We're doing this here because we want to control this only when saving + // the file. + while (oState._closedWindows.length) { + let i = oState._closedWindows.length - 1; + if (oState._closedWindows[i]._shouldRestore) { + delete oState._closedWindows[i]._shouldRestore; + oState.windows.unshift(oState._closedWindows.pop()); + } + else { + // We only need to go until we hit !needsRestore since we're going in reverse + break; + } + } + + if (pinnedOnly) { + // Save original resume_session_once preference for when quiting browser, + // otherwise session will be restored next time browser starts and we + // only want it to be restored in the case of a crash. + if (this._resume_session_once_on_shutdown == null) { + this._resume_session_once_on_shutdown = + this._prefBranch.getBoolPref("sessionstore.resume_session_once"); + this._prefBranch.setBoolPref("sessionstore.resume_session_once", true); + // flush the preference file so preference will be saved in case of a crash + Services.prefs.savePrefFile(null); + } + } + + // Persist the last session if we deferred restoring it + if (this._lastSessionState) + oState.lastSessionState = this._lastSessionState; + + // Make sure that we keep the previous session if we started with a single + // private window and no non-private windows have been opened, yet. + if (this._deferredInitialState) { + oState.windows = this._deferredInitialState.windows || []; + } + + this._saveStateObject(oState); + }, + + /** + * write a state object to disk + */ + _saveStateObject: function(aStateObj) { + let data = this._toJSONString(aStateObj); + + let stateString = this._createSupportsString(data); + Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); + data = stateString.data; + + // Don't touch the file if an observer has deleted all state data. + if (!data) { + return; + } + + let promise; + // If "sessionstore.resume_from_crash" is true, attempt to backup the + // session file first, before writing to it. + if (this._resume_from_crash) { + // Note that we do not have race conditions here as _SessionFile + // guarantees that any I/O operation is completed before proceeding to + // the next I/O operation. + // Note backup happens only once, on initial save. + promise = this._backupSessionFileOnce; + } else { + promise = Promise.resolve(); + } + + // Attempt to write to the session file (potentially, depending on + // "sessionstore.resume_from_crash" preference, after successful backup). + promise = promise.then(function onSuccess() { + // Write (atomically) to a session file, using a tmp file. + return _SessionFile.write(data); + }); + + // Once the session file is successfully updated, save the time stamp of the + // last save and notify the observers. + promise = promise.then(() => { + this._lastSaveTime = Date.now(); + Services.obs.notifyObservers(null, "sessionstore-state-write-complete", + ""); + }); + }, + + /* ........ Auxiliary Functions .............. */ + + // Wrap a string as a nsISupports + _createSupportsString: function(aData) { + let string = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + string.data = aData; + return string; + }, + + /** + * 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(aFunc) { + var windowsEnum = Services.wm.getEnumerator("navigator:browser"); + + while (windowsEnum.hasMoreElements()) { + var window = windowsEnum.getNext(); + if (window.__SSi && !window.closed) { + aFunc.call(this, window); + } + } + }, + + /** + * Returns most recent window + * @returns Window reference + */ + _getMostRecentBrowserWindow: function() { + var win = Services.wm.getMostRecentWindow("navigator:browser"); + if (!win) + return null; + if (!win.closed) + return win; + +#ifdef 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; +#else + var windowsEnum = + Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true); + while (windowsEnum.hasMoreElements()) { + win = windowsEnum.getNext(); + if (!win.closed) + return win; + } + return null; +#endif + }, + + /** + * 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() { + 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(aState) { + var argString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + argString.data = ""; + + // Build feature string + let features = "chrome,dialog=no,macsuppressanimation,all"; + let winState = aState.windows[0]; + WINDOW_ATTRIBUTES.forEach(function(aFeature) { + // Use !isNaN as an easy way to ignore sizemode and check for numbers + if (aFeature in winState && !isNaN(winState[aFeature])) + features += "," + aFeature + "=" + winState[aFeature]; + }); + + if (winState.isPrivate) { + features += ",private"; + } + + var window = + Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"), + "_blank", features, argString); + + do { + var ID = "window" + Math.random(); + } while (ID in this._statesToRestore); + this._statesToRestore[(window.__SS_restoreID = ID)] = aState; + + return window; + }, + + /** + * Whether or not to resume session, if not recovering from a crash. + * @returns bool + */ + _doResumeSession: function() { + return this._prefBranch.getIntPref("startup.page") == 3 || + this._prefBranch.getBoolPref("sessionstore.resume_session_once"); + }, + + /** + * whether the user wants to load any other page at startup + * (except the homepage) - needed for determining whether to overwrite the current tabs + * C.f.: nsBrowserContentHandler's defaultArgs implementation. + * @returns bool + */ + _isCmdLineEmpty: function(aWindow, aState) { + var pinnedOnly = aState.windows && + aState.windows.every(function(win) + win.tabs.every(function(tab) tab.pinned)); + + let hasFirstArgument = aWindow.arguments && aWindow.arguments[0]; + if (!pinnedOnly) { + let defaultArgs = Cc["@mozilla.org/browser/clh;1"]. + getService(Ci.nsIBrowserHandler).defaultArgs; + if (aWindow.arguments && + aWindow.arguments[0] && + aWindow.arguments[0] == defaultArgs) + hasFirstArgument = false; + } + + return !hasFirstArgument; + }, + + /** + * 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(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(aWindow, aAttribute) { + if (aAttribute == "sizemode") { + switch (aWindow.windowState) { + case aWindow.STATE_FULLSCREEN: + case aWindow.STATE_MAXIMIZED: + return "maximized"; + case aWindow.STATE_MINIMIZED: + return "minimized"; + default: + return "normal"; + } + } + + var dimension; + switch (aAttribute) { + case "width": + dimension = aWindow.outerWidth; + break; + case "height": + dimension = aWindow.outerHeight; + break; + default: + dimension = aAttribute in aWindow ? aWindow[aAttribute] : ""; + break; + } + + if (aWindow.windowState == aWindow.STATE_NORMAL) { + return dimension; + } + return aWindow.document.documentElement.getAttribute(aAttribute) || dimension; + }, + + /** + * Get nsIURI from string + * @param string + * @returns nsIURI + */ + _getURIFromString: function(aString) { + return Services.io.newURI(aString, null, null); + }, + + /** + * @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(aState, aRecentCrashes) { + const SIX_HOURS_IN_MS = 6 * 60 * 60 * 1000; + + // don't display the page when there's nothing to restore + let winData = aState.windows || null; + if (!winData || winData.length == 0) + return false; + + // don't wrap a single about:sessionrestore page + if (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(aTabState) { + // If the tab has only a transient about: history entry, no other + // session history, and no userTypedValue, then we don't actually want to + // store this tab's data. + return aTabState.entries.length && + !(aTabState.entries.length == 1 && + (aTabState.entries[0].url == "about:blank" || + aTabState.entries[0].url == "about:newtab") && + !aTabState.userTypedValue); + }, + + /** + * Determine if we can restore history into this tab. + * This will be false when a tab has been removed (usually between + * restoreHistoryPrecursor && restoreHistory) or if the tab is still marked + * as loading. + * + * @param aTab + * @returns boolean + */ + _canRestoreTabHistory: function(aTab) { + return aTab.parentNode && aTab.linkedBrowser && + aTab.linkedBrowser.__SS_tabStillLoading; + }, + + /** + * 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(state) { + // Make sure that we don't modify the global state as provided by + // nsSessionStartup.state. Converting the object to a JSON string and + // parsing it again is the easiest way to do that, although not the most + // efficient one. Deferred sessions that don't have automatic session + // restore enabled tend to be a lot smaller though so that this shouldn't + // be a big perf hit. + state = JSON.parse(JSON.stringify(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(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"); + // If we don't actually have any hosts, then we don't want to do anything. + if (!hosts.length) + return; + let cookieRegex = new RegExp(".*(" + hosts + ")"); + for (let cIndex = 0; cIndex < aWinState.cookies.length;) { + if (cookieRegex.test(aWinState.cookies[cIndex].host)) { + aTargetWinState.cookies = + aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1)); + continue; + } + cIndex++; + } + }, + + /** + * 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(aJSObject) { + return JSON.stringify(aJSObject); + }, + + _sendRestoreCompletedNotifications: function() { + // not all windows restored, yet + if (this._restoreCount > 1) { + this._restoreCount--; + return; + } + + // observers were already notified + if (this._restoreCount == -1) + return; + + // This was the last window restored at startup, notify observers. + Services.obs.notifyObservers(null, + this._browserSetState ? NOTIFY_BROWSER_STATE_RESTORED : NOTIFY_WINDOWS_RESTORED, + ""); + + this._browserSetState = false; + this._restoreCount = -1; + }, + + /** + * Set the given window's busy state + * @param aWindow the window + * @param aValue the window's busy state + */ + _setWindowStateBusyValue: + function(aWindow, aValue) { + + this._windows[aWindow.__SSi].busy = aValue; + + // Keep the to-be-restored state in sync because that is returned by + // getWindowState() as long as the window isn't loaded, yet. + if (!this._isWindowLoaded(aWindow)) { + let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0]; + stateToRestore.busy = aValue; + } + }, + + /** + * Set the given window's state to 'not busy'. + * @param aWindow the window + */ + _setWindowStateReady: function(aWindow) { + this._setWindowStateBusyValue(aWindow, false); + this._sendWindowStateEvent(aWindow, "Ready"); + }, + + /** + * Set the given window's state to 'busy'. + * @param aWindow the window + */ + _setWindowStateBusy: function(aWindow) { + this._setWindowStateBusyValue(aWindow, true); + this._sendWindowStateEvent(aWindow, "Busy"); + }, + + /** + * Dispatch an SSWindowState_____ event for the given window. + * @param aWindow the window + * @param aType the type of event, SSWindowState will be prepended to this string + */ + _sendWindowStateEvent: function(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(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(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(aString, aTabbrowser, aTab) { + if (aString == aTabbrowser.mStringBundle.getString("tabs.connecting")) { + aTabbrowser.setTabTitle(aTab); + [aString, aTab.label] = [aTab.label, aString]; + } + return aString; + }, + + /** + * Resize this._closedWindows to the value of the pref, except in the case + * where we don't have any non-popup windows on Windows and Linux. Then we must + * resize such that we have at least one non-popup window. + */ + _capClosedWindows : function() { + if (this._closedWindows.length <= this._max_windows_undo) + return; + let spliceTo = this._max_windows_undo; + 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); + }, + + _clearRestoringWindows: function() { + for (let i = 0; i < this._closedWindows.length; i++) { + delete this._closedWindows[i]._shouldRestore; + } + }, + + /** + * Reset state to prepare for a new session state to be restored. + */ + _resetRestoringState: function() { + TabRestoreQueue.reset(); + this._tabsRestoringCount = 0; + }, + + /** + * Reset the restoring state for a particular tab. This will be called when + * removing a tab or when a tab needs to be reset (it's being overwritten). + * + * @param aTab + * The tab that will be "reset" + */ + _resetTabRestoringState: function(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; + + aTab.removeAttribute("pending"); + browser.removeAttribute("pending"); + + // 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. + TabRestoreQueue.remove(aTab); + } + }, + + /** + * 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(aWindow) { + let tabbrowser = aWindow.gBrowser; + if (tabbrowser.mTabsProgressListeners.indexOf(gRestoreTabsProgressListener) == -1) + tabbrowser.addTabsProgressListener(gRestoreTabsProgressListener); + }, + + /** + * Attempt to remove the tabs progress listener from the window. + * + * @param aWindow + * The window from which to remove our progress listener from + */ + _removeTabsProgressListener: function(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) + aWindow.gBrowser.removeTabsProgressListener(gRestoreTabsProgressListener); + }, + + /** + * 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(aTab) { + let browser = aTab.linkedBrowser; + if (browser.__SS_shistoryListener) { + browser.webNavigation.sessionHistory. + removeSHistoryListener(browser.__SS_shistoryListener); + delete browser.__SS_shistoryListener; + } + } +}; + +/** + * Priority queue that keeps track of a list of tabs to restore and returns + * the tab we should restore next, based on priority rules. We decide between + * pinned, visible and hidden tabs in that and FIFO order. Hidden tabs are only + * restored with restore_hidden_tabs=true. + */ +var TabRestoreQueue = { + // The separate buckets used to store tabs. + tabs: {priority: [], visible: [], hidden: []}, + + // Preferences used by the TabRestoreQueue to determine which tabs + // are restored automatically and which tabs will be on-demand. + prefs: { + // Lazy getter that returns whether tabs are restored on demand. + get restoreOnDemand() { + let updateValue = () => { + let value = Services.prefs.getBoolPref(PREF); + let definition = {value: value, configurable: true}; + Object.defineProperty(this, "restoreOnDemand", definition); + return value; + } + + const PREF = "browser.sessionstore.restore_on_demand"; + Services.prefs.addObserver(PREF, updateValue, false); + return updateValue(); + }, + + // Lazy getter that returns whether pinned tabs are restored on demand. + get restorePinnedTabsOnDemand() { + let updateValue = () => { + let value = Services.prefs.getBoolPref(PREF); + let definition = {value: value, configurable: true}; + Object.defineProperty(this, "restorePinnedTabsOnDemand", definition); + return value; + } + + const PREF = "browser.sessionstore.restore_pinned_tabs_on_demand"; + Services.prefs.addObserver(PREF, updateValue, false); + return updateValue(); + }, + + // Lazy getter that returns whether we should restore hidden tabs. + get restoreHiddenTabs() { + let updateValue = () => { + let value = Services.prefs.getBoolPref(PREF); + let definition = {value: value, configurable: true}; + Object.defineProperty(this, "restoreHiddenTabs", definition); + return value; + } + + const PREF = "browser.sessionstore.restore_hidden_tabs"; + Services.prefs.addObserver(PREF, updateValue, false); + return updateValue(); + } + }, + + // Resets the queue and removes all tabs. + reset: function() { + this.tabs = {priority: [], visible: [], hidden: []}; + }, + + // Adds a tab to the queue and determines its priority bucket. + add: function(tab) { + let {priority, hidden, visible} = this.tabs; + + if (tab.pinned) { + priority.push(tab); + } else if (tab.hidden) { + hidden.push(tab); + } else { + visible.push(tab); + } + }, + + // Removes a given tab from the queue, if it's in there. + remove: function(tab) { + let {priority, hidden, visible} = this.tabs; + + // We'll always check priority first since we don't + // have an indicator if a tab will be there or not. + let set = priority; + let index = set.indexOf(tab); + + if (index == -1) { + set = tab.hidden ? hidden : visible; + index = set.indexOf(tab); + } + + if (index > -1) { + set.splice(index, 1); + } + }, + + // Returns and removes the tab with the highest priority. + shift: function() { + let set; + let {priority, hidden, visible} = this.tabs; + + let {restoreOnDemand, restorePinnedTabsOnDemand} = this.prefs; + let restorePinned = !(restoreOnDemand && restorePinnedTabsOnDemand); + if (restorePinned && priority.length) { + set = priority; + } else if (!restoreOnDemand) { + if (visible.length) { + set = visible; + } else if (this.prefs.restoreHiddenTabs && hidden.length) { + set = hidden; + } + } + + return set && set.shift(); + }, + + // Moves a given tab from the 'hidden' to the 'visible' bucket. + hiddenToVisible: function(tab) { + let {hidden, visible} = this.tabs; + let index = hidden.indexOf(tab); + + if (index > -1) { + hidden.splice(index, 1); + visible.push(tab); + } else { + throw new Error("restore queue: hidden tab not found"); + } + }, + + // Moves a given tab from the 'visible' to the 'hidden' bucket. + visibleToHidden: function(tab) { + let {visible, hidden} = this.tabs; + let index = visible.indexOf(tab); + + if (index > -1) { + visible.splice(index, 1); + hidden.push(tab); + } else { + throw new Error("restore queue: visible tab not found"); + } + } +}; + +// 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 set of tab attributes to persist. We will read a given list of tab +// attributes when collecting tab data and will re-set those attributes when +// the given tab data is restored to a new tab. +var TabAttributes = { + _attrs: new Set(), + + // We never want to directly read or write those attributes. + // 'image' should not be accessed directly but handled by using the + // gBrowser.getIcon()/setIcon() methods. + // 'pending' is used internal by sessionstore and managed accordingly. + // 'skipbackgroundnotify' is used internal by tabbrowser.xml. + _skipAttrs: new Set(["image", "pending", "skipbackgroundnotify"]), + + persist: function(name) { + if (this._attrs.has(name) || this._skipAttrs.has(name)) { + return false; + } + + this._attrs.add(name); + return true; + }, + + get: function(tab) { + let data = {}; + + for (let name of this._attrs) { + if (tab.hasAttribute(name)) { + data[name] = tab.getAttribute(name); + } + } + + return data; + }, + + set: function(tab, data = {}) { + // Clear attributes. + for (let name of this._attrs) { + tab.removeAttribute(name); + } + + // Set attributes. + for (let name in data) { + tab.setAttribute(name, data[name]); + } + } +}; + +// 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 = { + 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 && + 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 win = aBrowser.ownerDocument.defaultView; + let tab = win.gBrowser.getTabForBrowser(aBrowser); + SessionStoreInternal._resetTabRestoringState(tab); + SessionStoreInternal.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(aTab) { + this.tab = aTab; +} +SessionStoreSHistoryListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsISHistoryListener, + Ci.nsISupportsWeakReference + ]), + browser: null, + OnHistoryNewEntry: function(aNewURI) { }, + OnHistoryGoBack: function(aBackURI) { return true; }, + OnHistoryGoForward: function(aForwardURI) { return true; }, + OnHistoryGotoIndex: function(aIndex, aGotoURI) { return true; }, + OnHistoryPurge: function(aNumEntries) { return true; }, + 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 juet call restoreTab. That will remove + // the history listener and load the right URI. + SessionStoreInternal.restoreTab(this.tab); + // Returning false will stop the load that docshell is attempting. + return false; + } +} + +// See toolkit/forgetaboutsite/ForgetAboutSite.jsm +String.prototype.hasRootDomain = function hasRootDomain(aDomain) { + let index = this.indexOf(aDomain); + if (index == -1) + return false; + + if (this == aDomain) + return true; + + let prevChar = this[index - 1]; + return (index == (this.length - aDomain.length)) && + (prevChar == "." || prevChar == "/"); +} diff --git a/browser/components/sessionstore/XPathGenerator.jsm b/browser/components/sessionstore/XPathGenerator.jsm new file mode 100644 index 000000000..d0639ebb4 --- /dev/null +++ b/browser/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/. */ + +this.EXPORTED_SYMBOLS = ["XPathGenerator"]; + +this.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(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(aDocument, aQuery) { + let xptype = Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE; + return aDocument.evaluate(aQuery, aDocument, this.resolveNS, xptype, null).singleNodeValue; + }, + + /** + * Namespace resolver for the above XPath resolver + */ + resolveNS: function(aPrefix) { + return XPathGenerator.namespaceURIs[aPrefix] || null; + }, + + /** + * @returns valid XPath for the given node (usually just the local name itself) + */ + escapeName: function(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(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/browser/components/sessionstore/_SessionFile.jsm b/browser/components/sessionstore/_SessionFile.jsm new file mode 100644 index 000000000..173f6035d --- /dev/null +++ b/browser/components/sessionstore/_SessionFile.jsm @@ -0,0 +1,314 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["_SessionFile"]; + +/** + * Implementation of all the disk I/O required by the session store. + * This is a private API, meant to be used only by the session store. + * It will change. Do not use it for any other purpose. + * + * Note that this module implicitly depends on one of two things: + * 1. either the asynchronous file I/O system enqueues its requests + * and never attempts to simultaneously execute two I/O requests on + * the files used by this module from two distinct threads; or + * 2. the clients of this API are well-behaved and do not place + * concurrent requests to the files used by this module. + * + * Otherwise, we could encounter bugs, especially under Windows, + * e.g. if a request attempts to write sessionstore.js while + * another attempts to copy that file. + * + * This implementation uses OS.File, which guarantees property 1. + */ + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/Console.jsm"); + +// An encoder to UTF-8. +XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { + return new TextEncoder(); +}); +// A decoder. +XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { + return new TextDecoder(); +}); + +this._SessionFile = { + /** + * A promise fulfilled once initialization (either synchronous or + * asynchronous) is complete. + */ + promiseInitialized: function() { + return SessionFileInternal.promiseInitialized; + }, + /** + * Read the contents of the session file, asynchronously. + */ + read: function() { + return SessionFileInternal.read(); + }, + /** + * Read the contents of the session file, synchronously. + */ + syncRead: function() { + return SessionFileInternal.syncRead(); + }, + /** + * Write the contents of the session file, asynchronously. + */ + write: function(aData) { + return SessionFileInternal.write(aData); + }, + /** + * Create a backup copy, asynchronously. + */ + createBackupCopy: function() { + return SessionFileInternal.createBackupCopy(); + }, + /** + * Wipe the contents of the session file, asynchronously. + */ + wipe: function() { + return SessionFileInternal.wipe(); + } +}; + +Object.freeze(_SessionFile); + +/** + * Utilities for dealing with promises and Task.jsm + */ +const TaskUtils = { + /** + * Add logging to a promise. + * + * @param {Promise} promise + * @return {Promise} A promise behaving as |promise|, but with additional + * logging in case of uncaught error. + */ + captureErrors: function(promise) { + return promise.then( + null, + function onError(reason) { + console.error("Uncaught asynchronous error:", reason); + throw reason; + } + ); + }, + /** + * Spawn a new Task from a generator. + * + * This function behaves as |Task.spawn|, with the exception that it + * adds logging in case of uncaught error. For more information, see + * the documentation of |Task.jsm|. + * + * @param {generator} gen Some generator. + * @return {Promise} A promise built from |gen|, with the same semantics + * as |Task.spawn(gen)|. + */ + spawn: function spawn(gen) { + return this.captureErrors(Task.spawn(gen)); + } +}; + +var SessionFileInternal = { + /** + * A promise fulfilled once initialization is complete + */ + promiseInitialized: Promise.defer(), + + /** + * The path to sessionstore.js + */ + path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), + + /** + * The path to sessionstore.bak + */ + backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"), + + /** + * Utility function to safely read a file synchronously. + * @param aPath + * A path to read the file from. + * @returns string if successful, undefined otherwise. + */ + readAuxSync: function(aPath) { + let text; + try { + let file = new FileUtils.File(aPath); + let chan = NetUtil.newChannel({ + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true + }); + let stream = chan.open(); + text = NetUtil.readInputStreamToString(stream, stream.available(), + {charset: "utf-8"}); + } catch (e if e.result == Components.results.NS_ERROR_FILE_NOT_FOUND) { + // Ignore exceptions about non-existent files. + } catch (ex) { + // Any other error. + console.error("Uncaught error:", ex); + } finally { + return text; + } + }, + + /** + * Read the sessionstore file synchronously. + * + * This function is meant to serve as a fallback in case of race + * between a synchronous usage of the API and asynchronous + * initialization. + * + * In case if sessionstore.js file does not exist or is corrupted (something + * happened between backup and write), attempt to read the sessionstore.bak + * instead. + */ + syncRead: function() { + // First read the sessionstore.js. + let text = this.readAuxSync(this.path); + if (typeof text === "undefined") { + // If sessionstore.js does not exist or is corrupted, read sessionstore.bak. + text = this.readAuxSync(this.backupPath); + } + return text || ""; + }, + + /** + * Utility function to safely read a file asynchronously. + * @param aPath + * A path to read the file from. + * @param aReadOptions + * Read operation options. + * |outExecutionDuration| option will be reused and can be + * incrementally updated by the worker process. + * @returns string if successful, undefined otherwise. + */ + readAux: function(aPath, aReadOptions) { + let self = this; + return TaskUtils.spawn(function() { + let text; + try { + let bytes = yield OS.File.read(aPath, undefined, aReadOptions); + text = gDecoder.decode(bytes); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + // Any other error. + console.error("Uncaught error - with the file: " + self.path, ex); + } + throw new Task.Result(text); + }); + }, + + /** + * Read the sessionstore file asynchronously. + * + * In case sessionstore.js file does not exist or is corrupted (something + * happened between backup and write), attempt to read the sessionstore.bak + * instead. + */ + read: function() { + let self = this; + return TaskUtils.spawn(function task() { + // Specify |outExecutionDuration| option to hold the combined duration of + // the asynchronous reads off the main thread (of both sessionstore.js and + // sessionstore.bak, if necessary). If sessionstore.js does not exist or + // is corrupted, |outExecutionDuration| will register the time it took to + // attempt to read the file. It will then be subsequently incremented by + // the read time of sessionsore.bak. + let readOptions = { + outExecutionDuration: null + }; + // First read the sessionstore.js. + let text = yield self.readAux(self.path, readOptions); + if (typeof text === "undefined") { + // If sessionstore.js does not exist or is corrupted, read the + // sessionstore.bak. + text = yield self.readAux(self.backupPath, readOptions); + } + // Return either the content of the sessionstore.bak if it was read + // successfully or an empty string otherwise. + throw new Task.Result(text || ""); + }); + }, + + write: function(aData) { + let refObj = {}; + let self = this; + return TaskUtils.spawn(function task() { + let bytes = gEncoder.encode(aData); + + try { + let promise = OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"}); + yield promise; + } catch (ex) { + console.error("Could not write session state file: " + self.path, ex); + } + }); + }, + + createBackupCopy: function() { + let backupCopyOptions = { + outExecutionDuration: null + }; + let self = this; + return TaskUtils.spawn(function task() { + try { + yield OS.File.move(self.path, self.backupPath, backupCopyOptions); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not backup session state file: " + self.path, ex); + throw ex; + } + }); + }, + + wipe: function() { + let self = this; + return TaskUtils.spawn(function task() { + try { + yield OS.File.remove(self.path); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not remove session state file: " + self.path, ex); + throw ex; + } + + try { + yield OS.File.remove(self.backupPath); + } catch (ex if self._isNoSuchFile(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + console.error("Could not remove session state backup file: " + self.path, ex); + throw ex; + } + }); + }, + + _isNoSuchFile: function(aReason) { + return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; + } +}; diff --git a/browser/components/sessionstore/content/aboutSessionRestore.js b/browser/components/sessionstore/content/aboutSessionRestore.js new file mode 100644 index 000000000..7e21c97be --- /dev/null +++ b/browser/components/sessionstore/content/aboutSessionRestore.js @@ -0,0 +1,316 @@ +/* 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 Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +var gStateObject; +var gTreeData; + +// Page initialization + +window.onload = function() { + // the crashed session state is kept inside a textbox so that SessionStore picks it up + // (for when the tab is closed or the session crashes right again) + var sessionData = document.getElementById("sessionData"); + if (!sessionData.value) { + document.getElementById("errorTryAgain").disabled = true; + return; + } + + // remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0) + if (sessionData.value.charAt(0) == '(') + sessionData.value = sessionData.value.slice(1, -1); + try { + gStateObject = JSON.parse(sessionData.value); + } + catch (exJSON) { + var s = new Cu.Sandbox("about:blank", {sandboxName: 'aboutSessionRestore'}); + gStateObject = Cu.evalInSandbox("(" + sessionData.value + ")", s); + // If we couldn't parse the string with JSON.parse originally, make sure + // that the value in the textbox will be parsable. + sessionData.value = JSON.stringify(gStateObject); + } + + // make sure the data is tracked to be restored in case of a subsequent crash + var event = document.createEvent("UIEvents"); + event.initUIEvent("input", true, true, window, 0); + sessionData.dispatchEvent(event); + + initTreeView(); + + document.getElementById("errorTryAgain").focus(); +}; + +function 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 (let 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(function(aTabData, aIx) + gTreeData[t].tabs[aIx].checked); + else if (!gTreeData[t].checked) + // this window won't be restored at all + gStateObject.windows.splice(ix, 1); + ix--; + } + } + var stateString = JSON.stringify(gStateObject); + + var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + var top = getBrowserWindow(); + + // if there's only this page open, reuse the window for restoring the session + if (top.gBrowser.tabs.length == 1) { + ss.setWindowState(top, stateString, true); + return; + } + + // restore the session into a new window and close the current tab + var newWindow = top.openDialog(top.location, "_blank", "chrome,dialog=no,all"); + newWindow.addEventListener("load", function() { + newWindow.removeEventListener("load", arguments.callee, true); + ss.setWindowState(newWindow, stateString, true); + + var tabbrowser = top.gBrowser; + var tabIndex = tabbrowser.getBrowserIndexForDocument(document); + tabbrowser.removeTab(tabbrowser.tabs[tabIndex]); + }, true); +} + +function startNewSession() { + var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + if (prefBranch.getIntPref("browser.startup.page") == 0) + getBrowserWindow().gBrowser.loadURI("about:logopage"); + else + getBrowserWindow().BrowserHome(); +} + +function onListClick(aEvent) { + // don't react to right-clicks + if (aEvent.button == 2) + return; + + if (!treeView.treeBox) { + return; + } + var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY); + if (cell.col) { + // Restore this specific tab in the same window for middle/double/accel clicking + // on a tab's title. + let accelKey = aEvent.ctrlKey; + if ((aEvent.button == 1 || aEvent.button == 0 && aEvent.detail == 2 || accelKey) && + cell.col.id == "title" && + !treeView.isContainer(cell.row)) { + restoreSingleTab(cell.row, aEvent.shiftKey); + aEvent.stopPropagation(); + } + else if (cell.col.id == "restore") + toggleRowChecked(cell.row); + } +} + +function onListKeyDown(aEvent) { + switch (aEvent.keyCode) + { + case KeyEvent.DOM_VK_SPACE: + toggleRowChecked(document.getElementById("tabList").currentIndex); + break; + case KeyEvent.DOM_VK_RETURN: + var ix = document.getElementById("tabList").currentIndex; + if (aEvent.ctrlKey && !treeView.isContainer(ix)) + restoreSingleTab(ix, aEvent.shiftKey); + break; + case KeyEvent.DOM_VK_UP: + case KeyEvent.DOM_VK_DOWN: + case KeyEvent.DOM_VK_PAGE_UP: + case KeyEvent.DOM_VK_PAGE_DOWN: + case KeyEvent.DOM_VK_HOME: + case KeyEvent.DOM_VK_END: + aEvent.preventDefault(); // else the page scrolls unwantedly + 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) aItem.checked; + + if (treeView.isContainer(aIx)) { + // (un)check all tabs of this window as well + for (let tab of item.tabs) { + tab.checked = item.checked; + treeView.treeBox.invalidateRow(gTreeData.indexOf(tab)); + } + } + else { + // update the window's checkmark as well (0 means "partially checked") + item.parent.checked = item.parent.tabs.every(isChecked) ? true : + item.parent.tabs.some(isChecked) ? 0 : false; + treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent)); + } + + document.getElementById("errorTryAgain").disabled = !gTreeData.some(isChecked); +} + +function restoreSingleTab(aIx, aShifted) { + var tabbrowser = getBrowserWindow().gBrowser; + var newTab = tabbrowser.addTab(); + var item = gTreeData[aIx]; + + var ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + var tabState = gStateObject.windows[item.parent.ix] + .tabs[aIx - gTreeData.indexOf(item.parent) - 1]; + // ensure tab would be visible on the tabstrip. + tabState.hidden = false; + ss.setTabState(newTab, JSON.stringify(tabState)); + + // respect the preference as to whether to select the tab (the Shift key inverses) + var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + if (prefBranch.getBoolPref("browser.tabs.loadInBackground") != !aShifted) + tabbrowser.selectedTab = newTab; +} + +// Tree controller + +var treeView = { + treeBox: null, + selection: null, + + get rowCount() { return gTreeData.length; }, + setTree: function(treeBox) { this.treeBox = treeBox; }, + getCellText: function(idx, column) { return gTreeData[idx].label; }, + isContainer: function(idx) { return "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]; + if (winState.ix % 2 != 0) + return "alternate"; + + return ""; + }, + + getImageSrc: function(idx, column) { + if (column.id == "title") + return gTreeData[idx].src || null; + return null; + }, + + getProgressMode : function(idx, column) { }, + cycleHeader: function(column) { }, + cycleCell: function(idx, column) { }, + selectionChanged: function() { }, + performAction: function(action) { }, + performActionOnCell: function(action, index, column) { }, + getColumnProperties: function(column) { return ""; } +}; diff --git a/browser/components/sessionstore/content/aboutSessionRestore.xhtml b/browser/components/sessionstore/content/aboutSessionRestore.xhtml new file mode 100644 index 000000000..6b22250d7 --- /dev/null +++ b/browser/components/sessionstore/content/aboutSessionRestore.xhtml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +--> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd"> + %netErrorDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % restorepageDTD SYSTEM "chrome://browser/locale/aboutSessionRestore.dtd"> + %restorepageDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&restorepage.tabtitle;</title> + <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all"/> + <link rel="stylesheet" href="chrome://browser/skin/aboutSessionRestore.css" type="text/css" media="all"/> + <link rel="icon" type="image/png" href="chrome://global/skin/icons/warning-16.png"/> + + <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutSessionRestore.js"/> + </head> + + <body dir="&locale.dir;"> + + <!-- PAGE CONTAINER (for styling purposes only) --> + <div id="errorPageContainer"> + + <!-- Error Title --> + <div id="errorTitle"> + <h1 id="errorTitleText">&restorepage.errorTitle;</h1> + </div> + + <!-- LONG CONTENT (the section most likely to require scrolling) --> + <div id="errorLongContent"> + + <!-- Short Description --> + <div id="errorShortDesc"> + <p id="errorShortDescText">&restorepage.problemDesc;</p> + </div> + + <!-- Long Description (Note: See netError.dtd for used XHTML tags) --> + <div id="errorLongDesc"> + <p>&restorepage.tryThis;</p> + <ul> + <li>&restorepage.restoreSome;</li> + <li>&restorepage.startNew;</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" + onclick="onListClick(event);" onkeydown="onListKeyDown(event);" + _window_label="&restorepage.windowLabel;"> + <treecols> + <treecol cycler="true" 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"> +#ifdef XP_UNIX + <button id="errorCancel" label="&restorepage.closeButton;" + accesskey="&restorepage.close.access;" + oncommand="startNewSession();"/> + <button id="errorTryAgain" label="&restorepage.tryagainButton;" + accesskey="&restorepage.restore.access;" + oncommand="restoreSession();"/> +#else + <button id="errorTryAgain" label="&restorepage.tryagainButton;" + accesskey="&restorepage.restore.access;" + oncommand="restoreSession();"/> + <button id="errorCancel" label="&restorepage.closeButton;" + accesskey="&restorepage.close.access;" + oncommand="startNewSession();"/> +#endif + </hbox> + <!-- holds the session data for when the tab is closed --> + <input type="text" id="sessionData" style="display: none;"/> + </div> + + </body> +</html> diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js new file mode 100644 index 000000000..e3e956ef2 --- /dev/null +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -0,0 +1,40 @@ +/* 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/. */ + +function debug(msg) { + Services.console.logStringMessage("SessionStoreContent: " + msg); +} + +/** + * Listens for and handles content events that we need for the + * session store service to be notified of state changes in content. + */ +var EventListener = { + + DOM_EVENTS: [ + "pageshow", "change", "input" + ], + + init: function () { + this.DOM_EVENTS.forEach(e => addEventListener(e, this, true)); + }, + + handleEvent: function (event) { + switch (event.type) { + case "pageshow": + if (event.persisted) + sendAsyncMessage("SessionStore:pageshow"); + break; + case "input": + case "change": + sendAsyncMessage("SessionStore:input"); + break; + default: + debug("received unknown event '" + event.type + "'"); + break; + } + } +}; + +EventListener.init(); diff --git a/browser/components/sessionstore/jar.mn b/browser/components/sessionstore/jar.mn new file mode 100644 index 000000000..7ad408e4c --- /dev/null +++ b/browser/components/sessionstore/jar.mn @@ -0,0 +1,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/. + +browser.jar: +* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml) + content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js) + content/browser/content-sessionStore.js (content/content-sessionStore.js) diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build new file mode 100644 index 000000000..8167c7631 --- /dev/null +++ b/browser/components/sessionstore/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ['jar.mn'] + +XPIDL_SOURCES += [ + 'nsISessionStartup.idl', + 'nsISessionStore.idl', +] + +XPIDL_MODULE = 'sessionstore' + +EXTRA_COMPONENTS += [ + 'nsSessionStartup.js', + 'nsSessionStore.js', + 'nsSessionStore.manifest', +] + +EXTRA_JS_MODULES.sessionstore = [ + '_SessionFile.jsm', + 'DocumentUtils.jsm', + 'SessionStorage.jsm', + 'XPathGenerator.jsm', +] + +EXTRA_PP_JS_MODULES.sessionstore += ['SessionStore.jsm']
\ No newline at end of file diff --git a/browser/components/sessionstore/nsISessionStartup.idl b/browser/components/sessionstore/nsISessionStartup.idl new file mode 100644 index 000000000..a8e786d03 --- /dev/null +++ b/browser/components/sessionstore/nsISessionStartup.idl @@ -0,0 +1,59 @@ +/* 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(51f4b9f0-f3d2-11e2-bb62-2c24dd830245)] +interface nsISessionStartup: nsISupports +{ + /** + * Return a promise that is resolved once initialization + * is complete. + */ + readonly attribute jsval onceInitialized; + + // Get session state + readonly attribute jsval state; + + /** + * Determines whether there is a pending session restore and makes sure that + * we're initialized before returning. If we're not yet this will read the + * session file synchronously. + */ + boolean doRestore(); + + /** + * Returns whether we will restore a session that ends up replacing the + * homepage. The browser uses this to not start loading the homepage if + * we're going to stop its load anyway shortly after. + * + * This is meant to be an optimization for the average case that loading the + * session file finishes before we may want to start loading the default + * homepage. Should this be called before the session file has been read it + * will just return false. + */ + readonly attribute bool willOverrideHomepage; + + /** + * What type of session we're restoring. + * NO_SESSION There is no data available from the previous session + * RECOVER_SESSION The last session crashed. It will either be restored or + * about:sessionrestore will be shown. + * RESUME_SESSION The previous session should be restored at startup + * DEFER_SESSION The previous session is fine, but it shouldn't be restored + * without explicit action (with the exception of pinned tabs) + */ + const unsigned long NO_SESSION = 0; + const unsigned long RECOVER_SESSION = 1; + const unsigned long RESUME_SESSION = 2; + const unsigned long DEFER_SESSION = 3; + + readonly attribute unsigned long sessionType; +}; diff --git a/browser/components/sessionstore/nsISessionStore.idl b/browser/components/sessionstore/nsISessionStore.idl new file mode 100644 index 000000000..0490772a4 --- /dev/null +++ b/browser/components/sessionstore/nsISessionStore.idl @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIDOMWindow; +interface nsIDOMNode; + +/** + * nsISessionStore keeps track of the current browsing state - i.e. + * tab history, cookies, scroll state, form data, 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://browser/content/browser.xul . From overlays you can just pass the + * global |window| object to the API, though (or |top| from a sidebar). + * From elsewhere you can get browser windows through the nsIWindowMediator + * by looking for "navigator:browser" windows. + * + * * "Tabbrowser tabs" are all the child nodes of a browser window's + * |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|. + */ + +[scriptable, uuid(43ec216b-f002-4424-bfc5-fc555c87dbc4)] +interface nsISessionStore : nsISupports +{ + /** + * Initialize the service + */ + jsval 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 nsIDOMNode aTab); + + /** + * @param aTab is the tabbrowser tab whose state is to be set. + * @param aState is a JSON string representing a session state. + */ + void setTabState(in nsIDOMNode aTab, in AString aState); + + /** + * Duplicates a given tab as thoroughly as possible. + * + * @param aWindow is the browser window into which the tab will be duplicated. + * @param aTab is the tabbrowser tab to duplicate (can be from a different window). + * @param aDelta is the offset to the history entry to load in the duplicated tab. + * @returns a reference to the newly created tab. + */ + nsIDOMNode duplicateTab(in nsIDOMWindow aWindow, in nsIDOMNode aTab, + [optional] in long aDelta); + + /** + * Get the number of restore-able tabs for a browser window + */ + unsigned long getClosedTabCount(in nsIDOMWindow aWindow); + + /** + * Get closed tab data + * + * @param aWindow is the browser window for which to get closed tab data + * @returns a JSON string representing the list of closed tabs. + */ + AString getClosedTabData(in nsIDOMWindow aWindow); + + /** + * @param aWindow is the browser window to reopen a closed tab in. + * @param aIndex is the index of the tab to be restored (FIFO ordered). + * @returns a reference to the reopened tab. + */ + nsIDOMNode undoCloseTab(in nsIDOMWindow aWindow, in unsigned long aIndex); + + /** + * @param aWindow is the browser window associated with the closed tab. + * @param aIndex is the index of the closed tab to be removed (FIFO ordered). + */ + nsIDOMNode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex); + + /** + * Get the number of restore-able windows + */ + unsigned long getClosedWindowCount(); + + /** + * Get closed windows data + * + * @returns a JSON string representing the list of closed windows. + */ + AString getClosedWindowData(); + + /** + * @param aIndex is the index of the windows to be restored (FIFO ordered). + * @returns the nsIDOMWindow object of the reopened window + */ + nsIDOMWindow undoCloseWindow(in unsigned long aIndex); + + /** + * @param aIndex is the index of the closed window to be removed (FIFO ordered). + * + * @throws NS_ERROR_INVALID_ARG + * when aIndex does not map to a closed window + */ + nsIDOMNode forgetClosedWindow(in unsigned long aIndex); + + /** + * @param aWindow is the window to get the value for. + * @param aKey is the value's name. + * + * @returns A string value or an empty string if none is set. + */ + AString getWindowValue(in nsIDOMWindow aWindow, in AString aKey); + + /** + * @param aWindow is the browser window to set the value for. + * @param aKey is the value's name. + * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects). + */ + void setWindowValue(in nsIDOMWindow aWindow, in AString aKey, in 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 nsIDOMNode aTab, in AString aKey); + + /** + * @param aTab is the tabbrowser tab to set the value for. + * @param aKey is the value's name. + * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects). + */ + void setTabValue(in nsIDOMNode aTab, in AString aKey, in AString aStringValue); + + /** + * @param aTab is the tabbrowser tab to get the value for. + * @param aKey is the value's name. + */ + void deleteTabValue(in nsIDOMNode aTab, in AString aKey); + + /** + * @param aName is the name of the attribute to save/restore for all tabbrowser tabs. + */ + void persistTabAttribute(in AString aName); +}; diff --git a/browser/components/sessionstore/nsSessionStartup.js b/browser/components/sessionstore/nsSessionStartup.js new file mode 100644 index 000000000..13e13ecdb --- /dev/null +++ b/browser/components/sessionstore/nsSessionStartup.js @@ -0,0 +1,296 @@ +/* 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. If the auto-start Private Browsing + * mode is active, however, the session is never restored. + * + * 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 its 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 ::::::::::::::: */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile", + "resource:///modules/sessionstore/_SessionFile.jsm"); + +const STATE_RUNNING_STR = "running"; + +function debug(aMsg) { + aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n"); + Services.console.logStringMessage(aMsg); +} + +var gOnceInitializedDeferred = Promise.defer(); + +/* :::::::: The Service ::::::::::::::: */ + +function SessionStartup() { +} + +SessionStartup.prototype = { + + // the state to restore at startup + _initialState: null, + _sessionType: Ci.nsISessionStartup.NO_SESSION, + _initialized: false, + +/* ........ Global Event Handlers .............. */ + + /** + * Initialize the component + */ + init: function() { + // do not need to initialize anything in auto-started private browsing sessions + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + this._initialized = true; + gOnceInitializedDeferred.resolve(); + return; + } + + if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") || + Services.prefs.getIntPref("browser.startup.page") == 3) { + this._ensureInitialized(); + } else { + _SessionFile.read().then( + this._onSessionFileRead.bind(this) + ); + } + }, + + // Wrap a string as a nsISupports + _createSupportsString: function(aData) { + let string = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + string.data = aData; + return string; + }, + + _onSessionFileRead: function(aStateString) { + if (this._initialized) { + // Initialization is complete, nothing else to do + return; + } + try { + this._initialized = true; + + // Let observers modify the state before it is used + let supportsStateString = this._createSupportsString(aStateString); + Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", ""); + aStateString = supportsStateString.data; + + // No valid session found. + if (!aStateString) { + this._sessionType = Ci.nsISessionStartup.NO_SESSION; + return; + } + + // parse the session state into a JS object + // remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0) + if (aStateString.charAt(0) == '(') + aStateString = aStateString.slice(1, -1); + let corruptFile = false; + try { + this._initialState = JSON.parse(aStateString); + } + catch (ex) { + debug("The session file contained un-parse-able JSON: " + ex); + // This is not valid JSON, but this might still be valid JavaScript, + // as used in FF2/FF3, so we need to eval. + // evalInSandbox will throw if aStateString is not parse-able. + try { + var s = new Cu.Sandbox("about:blank", {sandboxName: 'nsSessionStartup'}); + this._initialState = Cu.evalInSandbox("(" + aStateString + ")", s); + } catch(ex) { + debug("The session file contained un-eval-able JSON: " + ex); + corruptFile = true; + } + } + let doResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); + let doResumeSession = doResumeSessionOnce || + Services.prefs.getIntPref("browser.startup.page") == 3; + + // If this is a normal restore then throw away any previous session + if (!doResumeSessionOnce) + delete this._initialState.lastSessionState; + + let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"); + 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 + + Services.obs.addObserver(this, "sessionstore-windows-restored", true); + + if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) + Services.obs.addObserver(this, "browser:purge-session-history", true); + + } finally { + // We're ready. Notify everyone else. + Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); + gOnceInitializedDeferred.resolve(); + } + }, + + /** + * Handle notifications + */ + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "app-startup": + Services.obs.addObserver(this, "final-ui-startup", true); + Services.obs.addObserver(this, "quit-application", true); + break; + case "final-ui-startup": + Services.obs.removeObserver(this, "final-ui-startup"); + Services.obs.removeObserver(this, "quit-application"); + this.init(); + break; + case "quit-application": + // no reason for initializing at this point (cf. bug 409115) + Services.obs.removeObserver(this, "final-ui-startup"); + Services.obs.removeObserver(this, "quit-application"); + if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) + Services.obs.removeObserver(this, "browser:purge-session-history"); + break; + case "sessionstore-windows-restored": + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + // free _initialState after nsSessionStore is done with it + this._initialState = null; + break; + case "browser:purge-session-history": + Services.obs.removeObserver(this, "browser:purge-session-history"); + // reset all state on sanitization + this._sessionType = Ci.nsISessionStartup.NO_SESSION; + break; + } + }, + +/* ........ Public API ................*/ + + get onceInitialized() { + return gOnceInitializedDeferred.promise; + }, + + /** + * Get the session state as a jsval + */ + get state() { + this._ensureInitialized(); + return this._initialState; + }, + + /** + * Determines whether there is a pending session restore and makes sure that + * we're initialized before returning. If we're not yet this will read the + * session file synchronously. + * @returns bool + */ + doRestore: function() { + this._ensureInitialized(); + return this._willRestore(); + }, + + /** + * Determines whether there is a pending session restore. + * @returns bool + */ + _willRestore: function() { + return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION || + this._sessionType == Ci.nsISessionStartup.RESUME_SESSION; + }, + + /** + * Returns whether we will restore a session that ends up replacing the + * homepage. The browser uses this to not start loading the homepage if + * we're going to stop its load anyway shortly after. + * + * This is meant to be an optimization for the average case that loading the + * session file finishes before we may want to start loading the default + * homepage. Should this be called before the session file has been read it + * will just return false. + * + * @returns bool + */ + get willOverrideHomepage() { + if (this._initialState && this._willRestore()) { + let windows = this._initialState.windows || null; + // If there are valid windows with not only pinned tabs, signal that we + // will override the default homepage by restoring a session. + return windows && windows.some(w => w.tabs.some(t => !t.pinned)); + } + return false; + }, + + /** + * Get the type of pending session store, if any. + */ + get sessionType() { + this._ensureInitialized(); + return this._sessionType; + }, + + // Ensure that initialization is complete. + // If initialization is not complete yet, fall back to a synchronous + // initialization and kill ongoing asynchronous initialization + _ensureInitialized: function() { + try { + if (this._initialized) { + // Initialization is complete, nothing else to do + return; + } + let contents = _SessionFile.syncRead(); + this._onSessionFileRead(contents); + } catch(ex) { + debug("ensureInitialized: could not read session " + ex + ", " + ex.stack); + throw ex; + } + }, + + /* ........ QueryInterface .............. */ + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference, + Ci.nsISessionStartup]), + classID: Components.ID("{ec7a6c20-e081-11da-8ad9-0800200c9a66}") +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]); diff --git a/browser/components/sessionstore/nsSessionStore.js b/browser/components/sessionstore/nsSessionStore.js new file mode 100644 index 000000000..38713d500 --- /dev/null +++ b/browser/components/sessionstore/nsSessionStore.js @@ -0,0 +1,37 @@ +/* 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. + */ + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/sessionstore/SessionStore.jsm"); + +function SessionStoreService() {} + +// The SessionStore module's object is frozen. We need to modify our prototype +// and add some properties so let's just copy the SessionStore object. +Object.keys(SessionStore).forEach(function (aName) { + let desc = Object.getOwnPropertyDescriptor(SessionStore, aName); + Object.defineProperty(SessionStoreService.prototype, aName, desc); +}); + +SessionStoreService.prototype.classID = + Components.ID("{5280606b-2510-4fe0-97ef-9b5a22eafe6b}"); +SessionStoreService.prototype.QueryInterface = + XPCOMUtils.generateQI([Ci.nsISessionStore]); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStoreService]); diff --git a/browser/components/sessionstore/nsSessionStore.manifest b/browser/components/sessionstore/nsSessionStore.manifest new file mode 100644 index 000000000..0501afeb2 --- /dev/null +++ b/browser/components/sessionstore/nsSessionStore.manifest @@ -0,0 +1,5 @@ +component {5280606b-2510-4fe0-97ef-9b5a22eafe6b} nsSessionStore.js +contract @mozilla.org/browser/sessionstore;1 {5280606b-2510-4fe0-97ef-9b5a22eafe6b} +component {ec7a6c20-e081-11da-8ad9-0800200c9a66} nsSessionStartup.js +contract @mozilla.org/browser/sessionstartup;1 {ec7a6c20-e081-11da-8ad9-0800200c9a66} +category app-startup nsSessionStartup service,@mozilla.org/browser/sessionstartup;1 |