diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 972d4659dc6..81371c76212 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -860,8 +860,6 @@ pref("browser.sessionstore.restore_pinned_tabs_on_demand", false); pref("browser.sessionstore.upgradeBackup.latestBuildID", ""); // End-users should not run sessionstore in debug mode pref("browser.sessionstore.debug", false); -// Enable asynchronous data collection by default. -pref("browser.sessionstore.async", true); // allow META refresh by default pref("accessibility.blockautorefresh", false); diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js index cb77583be36..f1db92e5745 100644 --- a/browser/components/sessionstore/content/content-sessionStore.js +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -2,19 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - function debug(msg) { Services.console.logStringMessage("SessionStoreContent: " + msg); } Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); -XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", - "resource:///modules/sessionstore/SessionHistory.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", - "resource:///modules/sessionstore/SessionStorage.jsm"); - /** * Listens for and handles content events that we need for the * session store service to be notified of state changes in content. @@ -62,37 +55,7 @@ let EventListener = { } } }; - -/** - * Listens for and handles messages sent by the session store service. - */ -let MessageListener = { - - MESSAGES: [ - "SessionStore:collectSessionHistory", - "SessionStore:collectSessionStorage" - ], - - init: function () { - this.MESSAGES.forEach(m => addMessageListener(m, this)); - }, - - receiveMessage: function ({name, data: {id}}) { - switch (name) { - case "SessionStore:collectSessionHistory": - let history = SessionHistory.read(docShell); - sendAsyncMessage(name, {id: id, data: history}); - break; - case "SessionStore:collectSessionStorage": - let storage = SessionStorage.serialize(docShell); - sendAsyncMessage(name, {id: id, data: storage}); - break; - default: - debug("received unknown message '" + name + "'"); - break; - } - } -}; +EventListener.init(); let ProgressListener = { init: function() { @@ -111,7 +74,4 @@ let ProgressListener = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]) }; - -EventListener.init(); -MessageListener.init(); ProgressListener.init(); diff --git a/browser/components/sessionstore/src/Messenger.jsm b/browser/components/sessionstore/src/Messenger.jsm deleted file mode 100644 index f0af898f882..00000000000 --- a/browser/components/sessionstore/src/Messenger.jsm +++ /dev/null @@ -1,71 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this file, -* You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["Messenger"]; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/Promise.jsm", this); -Cu.import("resource://gre/modules/Timer.jsm", this); - -/** - * The external API exported by this module. - */ -this.Messenger = Object.freeze({ - send: function (tab, type, options = {}) { - return MessengerInternal.send(tab, type, options); - } -}); - -/** - * A module that handles communication between the main and content processes. - */ -let MessengerInternal = { - // The id of the last message we sent. This is used to assign a unique ID to - // every message we send to handle multiple responses from the same browser. - _latestMessageID: 0, - - /** - * Sends a message to the given tab and waits for a response. - * - * @param tab - * tabbrowser tab - * @param type - * {string} the type of the message - * @param options (optional) - * {timeout: int} to set the timeout in milliseconds - * @return {Promise} A promise that will resolve to the response message or - * be reject when timing out. - */ - send: function (tab, type, options = {}) { - let browser = tab.linkedBrowser; - let mm = browser.messageManager; - let deferred = Promise.defer(); - let id = ++this._latestMessageID; - let timeout; - - function onMessage({data: {id: mid, data}}) { - if (mid == id) { - mm.removeMessageListener(type, onMessage); - clearTimeout(timeout); - deferred.resolve(data); - } - } - - mm.addMessageListener(type, onMessage); - mm.sendAsyncMessage(type, {id: id}); - - function onTimeout() { - mm.removeMessageListener(type, onMessage); - deferred.reject(new Error("Timed out while waiting for a " + type + " " + - "response message.")); - } - - let delay = (options && options.timeout) || 5000; - timeout = setTimeout(onTimeout, delay); - return deferred.promise; - } -}; diff --git a/browser/components/sessionstore/src/PrivacyLevel.jsm b/browser/components/sessionstore/src/PrivacyLevel.jsm deleted file mode 100644 index 1efd7595fdd..00000000000 --- a/browser/components/sessionstore/src/PrivacyLevel.jsm +++ /dev/null @@ -1,82 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this file, -* You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["PrivacyLevel"]; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/Services.jsm"); - -const PREF_NORMAL = "browser.sessionstore.privacy_level"; -const PREF_DEFERRED = "browser.sessionstore.privacy_level_deferred"; - -// The following constants represent the different possible privacy levels that -// can be set by the user and that we need to consider when collecting text -// data, cookies, and POSTDATA. -// -// Collect data from all sites (http and https). -const PRIVACY_NONE = 0; -// Collect data from unencrypted sites (http), only. -const PRIVACY_ENCRYPTED = 1; -// Collect no data. -const PRIVACY_FULL = 2; - -/** - * Returns whether we will resume the session automatically on next startup. - */ -function willResumeAutomatically() { - return Services.prefs.getIntPref("browser.startup.page") == 3 || - Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); -} - -/** - * Determines the current privacy level as set by the user. - * - * @param isPinned - * Whether to return the privacy level for pinned tabs. - * @return {int} The privacy level as read from the user's preferences. - */ -function getCurrentLevel(isPinned) { - let pref = PREF_NORMAL; - - // If we're in the process of quitting and we're not autoresuming the session - // then we will use the deferred privacy level for non-pinned tabs. - if (!isPinned && Services.startup.shuttingDown && !willResumeAutomatically()) { - pref = PREF_DEFERRED; - } - - return Services.prefs.getIntPref(pref); -} - -/** - * The external API as exposed by this module. - */ -let PrivacyLevel = Object.freeze({ - /** - * Checks whether we're allowed to save data for a specific site. - * - * @param {isHttps: boolean, isPinned: boolean} - * An object that must have two properties: 'isHttps' and 'isPinned'. - * 'isHttps' tells whether the site us secure communication (HTTPS). - * 'isPinned' tells whether the site is loaded in a pinned tab. - * @return {bool} Whether we can save data for the specified site. - */ - canSave: function ({isHttps, isPinned}) { - let level = getCurrentLevel(isPinned); - - // Never save any data when full privacy is requested. - if (level == PRIVACY_FULL) { - return false; - } - - // Don't save data for encrypted sites when requested. - if (isHttps && level == PRIVACY_ENCRYPTED) { - return false; - } - - return true; - } -}); diff --git a/browser/components/sessionstore/src/SessionCookies.jsm b/browser/components/sessionstore/src/SessionCookies.jsm index 037da33865d..33006dd7f81 100644 --- a/browser/components/sessionstore/src/SessionCookies.jsm +++ b/browser/components/sessionstore/src/SessionCookies.jsm @@ -12,8 +12,8 @@ const Ci = Components.interfaces; Cu.import("resource://gre/modules/Services.jsm", this); Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", - "resource:///modules/sessionstore/PrivacyLevel.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", + "resource:///modules/sessionstore/SessionStore.jsm"); // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision. const MAX_EXPIRY = Math.pow(2, 62); @@ -67,8 +67,8 @@ let SessionCookiesInternal = { for (let cookie of CookieStore.getCookiesForHost(host)) { // _getCookiesForHost() will only return hosts with the right privacy // rules, so there is no need to do anything special with this call - // to PrivacyLevel.canSave(). - if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) { + // to checkPrivacyLevel(). + if (SessionStore.checkPrivacyLevel(cookie.secure, isPinned)) { cookies.push(cookie); } } @@ -209,7 +209,7 @@ let SessionCookiesInternal = { // case testing scheme will be sufficient. if (/https?/.test(scheme) && !hosts[host] && (!checkPrivacy || - PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) { + SessionStore.checkPrivacyLevel(scheme == "https", isPinned))) { // By setting this to true or false, we can determine when looking at // the host in update() if we should check for privacy. hosts[host] = isPinned; diff --git a/browser/components/sessionstore/src/SessionHistory.jsm b/browser/components/sessionstore/src/SessionHistory.jsm deleted file mode 100644 index 65e8a890d91..00000000000 --- a/browser/components/sessionstore/src/SessionHistory.jsm +++ /dev/null @@ -1,272 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public -* License, v. 2.0. If a copy of the MPL was not distributed with this file, -* You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["SessionHistory"]; - -const Cu = Components.utils; -const Cc = Components.classes; -const Ci = Components.interfaces; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", - "resource:///modules/sessionstore/PrivacyLevel.jsm"); - -function debug(msg) { - Services.console.logStringMessage("SessionHistory: " + msg); -} - -// The preference value that determines how much post data to save. -XPCOMUtils.defineLazyGetter(this, "gPostData", function () { - const PREF = "browser.sessionstore.postdata"; - - // Observer that updates the cached value when the preference changes. - Services.prefs.addObserver(PREF, () => { - this.gPostData = Services.prefs.getIntPref(PREF); - }, false); - - return Services.prefs.getIntPref(PREF); -}); - -/** - * The external API exported by this module. - */ -this.SessionHistory = Object.freeze({ - read: function (docShell, includePrivateData) { - return SessionHistoryInternal.read(docShell, includePrivateData); - } -}); - -/** - * The internal API for the SessionHistory module. - */ -let SessionHistoryInternal = { - /** - * Collects session history data for a given docShell. - * - * @param docShell - * The docShell that owns the session history. - * @param includePrivateData (optional) - * True to always include private data and skip any privacy checks. - */ - read: function (docShell, includePrivateData = false) { - let data = {entries: []}; - let isPinned = docShell.isAppTab; - let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); - let history = webNavigation.sessionHistory; - - if (history && history.count > 0) { - try { - for (let i = 0; i < history.count; i++) { - let shEntry = history.getEntryAtIndex(i, false); - let entry = this._serializeEntry(shEntry, includePrivateData, isPinned); - data.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, print an error - // message, and still save sessionstore.js. - debug("SessionStore failed gathering complete history " + - "for the focused window/tab. See bug 669196."); - } - data.index = history.index + 1; - } else { - let uri = webNavigation.currentURI.spec; - // We landed here because the history is inaccessible or there are no - // history entries. In that case we should at least record the docShell's - // current URL as a single history entry. If the URL is not about:blank - // or it's a blank tab that was modified (like a custom newtab page), - // record it. For about:blank we explicitly want an empty array without - // an 'index' property to denote that there are no history entries. - if (uri != "about:blank" || webNavigation.document.body.hasChildNodes()) { - data.entries.push({ url: uri }); - data.index = 1; - } - } - - return data; - }, - - /** - * Get an object that is a serialized representation of a History entry. - * - * @param shEntry - * nsISHEntry instance - * @param includePrivateData - * Always return privacy sensitive data (use with care). - * @param isPinned - * The tab is pinned and should be treated differently for privacy. - * @return object - */ - _serializeEntry: function (shEntry, includePrivateData, isPinned) { - let entry = { url: shEntry.URI.spec }; - - // Save some bytes and don't include the title property - // if that's identical to the current entry's URL. - if (shEntry.title && shEntry.title != entry.url) { - entry.title = shEntry.title; - } - if (shEntry.isSubFrame) { - entry.subframe = true; - } - - let cacheKey = shEntry.cacheKey; - if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && - cacheKey.data != 0) { - // XXXbz would be better to have cache keys implement - // nsISerializable or something. - entry.cacheKey = cacheKey.data; - } - entry.ID = shEntry.ID; - entry.docshellID = shEntry.docshellID; - - // We will include the property only if it's truthy to save a couple of - // bytes when the resulting object is stringified and saved to disk. - if (shEntry.referrerURI) - entry.referrer = shEntry.referrerURI.spec; - - if (shEntry.srcdocData) - entry.srcdocData = shEntry.srcdocData; - - if (shEntry.isSrcdocEntry) - entry.isSrcdocEntry = shEntry.isSrcdocEntry; - - if (shEntry.contentType) - entry.contentType = shEntry.contentType; - - let x = {}, y = {}; - shEntry.getScrollPosition(x, y); - if (x.value != 0 || y.value != 0) - entry.scroll = x.value + "," + y.value; - - // Collect post data for the current history entry. - try { - let postdata = this._serializePostData(shEntry, isPinned); - if (postdata) { - entry.postdata_b64 = postdata; - } - } catch (ex) { - // POSTDATA is tricky - especially since some extensions don't get it right - debug("Failed serializing post data: " + ex); - } - - // Collect owner data for the current history entry. - try { - let owner = this._serializeOwner(shEntry); - if (owner) { - entry.owner_b64 = owner; - } - } catch (ex) { - // Not catching anything specific here, just possible errors - // from writeCompoundObject() and the like. - debug("Failed serializing owner data: " + ex); - } - - entry.docIdentifier = shEntry.BFCacheEntry.ID; - - if (shEntry.stateData != null) { - entry.structuredCloneState = shEntry.stateData.getDataAsBase64(); - entry.structuredCloneVersion = shEntry.stateData.formatVersion; - } - - if (!(shEntry instanceof Ci.nsISHContainer)) { - return entry; - } - - if (shEntry.childCount > 0) { - let children = []; - for (let i = 0; i < shEntry.childCount; i++) { - let child = shEntry.GetChildAt(i); - - if (child) { - // Don't try to restore framesets containing wyciwyg URLs. - // (cf. bug 424689 and bug 450595) - if (child.URI.schemeIs("wyciwyg")) { - children.length = 0; - break; - } - - children.push(this._serializeEntry(child, includePrivateData, isPinned)); - } - } - - if (children.length) { - entry.children = children; - } - } - - return entry; - }, - - /** - * Serialize post data contained in the given session history entry. - * - * @param shEntry - * The session history entry. - * @param isPinned - * Whether the docShell is owned by a pinned tab. - * @return The base64 encoded post data. - */ - _serializePostData: function (shEntry, isPinned) { - let isHttps = shEntry.URI.schemeIs("https"); - if (!shEntry.postData || !gPostData || - !PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) { - return null; - } - - shEntry.postData.QueryInterface(Ci.nsISeekableStream) - .seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); - let stream = Cc["@mozilla.org/binaryinputstream;1"] - .createInstance(Ci.nsIBinaryInputStream); - stream.setInputStream(shEntry.postData); - let postBytes = stream.readByteArray(stream.available()); - let postdata = String.fromCharCode.apply(null, postBytes); - if (gPostData != -1 && - postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length > gPostData) { - return null; - } - - // We can stop doing base64 encoding once our serialization into JSON - // is guaranteed to handle all chars in strings, including embedded - // nulls. - return btoa(postdata); - }, - - /** - * Serialize owner data contained in the given session history entry. - * - * @param shEntry - * The session history entry. - * @return The base64 encoded owner data. - */ - _serializeOwner: function (shEntry) { - if (!shEntry.owner) { - return null; - } - - let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"]. - createInstance(Ci.nsIObjectOutputStream); - let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); - pipe.init(false, false, 0, 0xffffffff, null); - binaryStream.setOutputStream(pipe.outputStream); - binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true); - binaryStream.close(); - - // Now we want to read the data from the pipe's input end and encode it. - let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"]. - createInstance(Ci.nsIBinaryInputStream); - scriptableStream.setInputStream(pipe.inputStream); - let ownerBytes = - 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. - return btoa(String.fromCharCode.apply(null, ownerBytes)); - } -}; diff --git a/browser/components/sessionstore/src/SessionSaver.jsm b/browser/components/sessionstore/src/SessionSaver.jsm index c8682303e42..b1977d9727c 100644 --- a/browser/components/sessionstore/src/SessionSaver.jsm +++ b/browser/components/sessionstore/src/SessionSaver.jsm @@ -151,7 +151,7 @@ let SessionSaverInternal = { delay = Math.max(this._lastSaveTime + gInterval - Date.now(), delay, 0); // Schedule a state save. - this._timeoutID = setTimeout(() => this._saveStateAsync(), delay); + this._timeoutID = setTimeout(() => this._saveState(), delay); }, /** @@ -186,7 +186,8 @@ let SessionSaverInternal = { * update the corresponding caches. */ _saveState: function (forceUpdateAllWindows = false) { - // Cancel any pending timeouts. + // Cancel any pending timeouts or just clear + // the timeout if this is why we've been called. this.cancel(); stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS"); @@ -245,33 +246,6 @@ let SessionSaverInternal = { this._writeState(state); }, - /** - * Saves the current session state. Collects data asynchronously and calls - * _saveState() to collect data again (with a cache hit rate of hopefully - * 100%) and write to disk afterwards. - */ - _saveStateAsync: function () { - // Allow scheduling delayed saves again. - this._timeoutID = null; - - // Check whether asynchronous data collection is disabled. - if (!Services.prefs.getBoolPref("browser.sessionstore.async")) { - this._saveState(); - return; - } - - // Update the last save time to make sure we wait at least another interval - // length until we call _saveStateAsync() again. - this.updateLastSaveTime(); - - // Save state synchronously after all tab caches have been filled. The data - // for the tab caches is collected asynchronously. We will reuse this - // cached data if the tab hasn't been invalidated in the meantime. In that - // case we will just fall back to synchronous data collection for single - // tabs. - SessionStore.fillTabCachesAsynchronously().then(() => this._saveState()); - }, - /** * Write the given state object to disk. */ diff --git a/browser/components/sessionstore/src/SessionStorage.jsm b/browser/components/sessionstore/src/SessionStorage.jsm index 119b4a1003f..5f72925bd09 100644 --- a/browser/components/sessionstore/src/SessionStorage.jsm +++ b/browser/components/sessionstore/src/SessionStorage.jsm @@ -7,13 +7,12 @@ this.EXPORTED_SYMBOLS = ["SessionStorage"]; const Cu = Components.utils; -const Ci = Components.interfaces; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", - "resource:///modules/sessionstore/PrivacyLevel.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", + "resource:///modules/sessionstore/SessionStore.jsm"); this.SessionStorage = { /** @@ -52,17 +51,16 @@ let DomStorage = { read: function DomStorage_read(aDocShell, aFullData) { let data = {}; let isPinned = aDocShell.isAppTab; - let webNavigation = aDocShell.QueryInterface(Ci.nsIWebNavigation); - let shistory = webNavigation.sessionHistory; + let shistory = aDocShell.sessionHistory; - for (let i = 0; shistory && i < shistory.count; i++) { + 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 || PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) { + let isHTTPS = principal.URI && principal.URI.schemeIs("https"); + if (aFullData || SessionStore.checkPrivacyLevel(isHTTPS, isPinned)) { let origin = principal.jarPrefix + principal.origin; // Don't read a host twice. @@ -92,8 +90,8 @@ let DomStorage = { let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager); // 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. + // domstorage event, which in this case has no consumer. Prevention of events in case + // of missing documentURI will be solved in a followup bug to bug 600307. let storage = storageManager.createStorage(principal, "", aDocShell.usePrivateBrowsing); for (let [key, value] in Iterator(data)) { diff --git a/browser/components/sessionstore/src/SessionStore.jsm b/browser/components/sessionstore/src/SessionStore.jsm index 859ed43c7bd..ce710982f9f 100644 --- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -21,6 +21,10 @@ 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"; @@ -82,6 +86,8 @@ const MS_PER_DAY = 1000.0 * 60.0 * 60.0 * 24.0; 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/TelemetryTimestamps.jsm", this); Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); Cu.import("resource://gre/modules/osfile.jsm", this); @@ -111,31 +117,16 @@ let gDocShellCapabilities = (function () { }; })(); -/** - * Get nsIURI from string - * @param string - * @returns nsIURI - */ -function makeURI(aString) { - return Services.io.newURI(aString, null, null); -} - XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager", "resource:///modules/devtools/scratchpad-manager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils", "resource:///modules/sessionstore/DocumentUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Messenger", - "resource:///modules/sessionstore/Messenger.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", - "resource:///modules/sessionstore/PrivacyLevel.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver", "resource:///modules/sessionstore/SessionSaver.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", "resource:///modules/sessionstore/SessionStorage.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies", "resource:///modules/sessionstore/SessionCookies.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", - "resource:///modules/sessionstore/SessionHistory.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile", "resource:///modules/sessionstore/_SessionFile.jsm"); @@ -273,12 +264,12 @@ this.SessionStore = { SessionStoreInternal.restoreLastSession(); }, - getCurrentState: function (aUpdateAll) { - return SessionStoreInternal.getCurrentState(aUpdateAll); + checkPrivacyLevel: function ss_checkPrivacyLevel(aIsHTTPS, aUseDefaultPref) { + return SessionStoreInternal.checkPrivacyLevel(aIsHTTPS, aUseDefaultPref); }, - fillTabCachesAsynchronously: function () { - return SessionStoreInternal.fillTabCachesAsynchronously(); + getCurrentState: function (aUpdateAll) { + return SessionStoreInternal.getCurrentState(aUpdateAll); }, /** @@ -401,6 +392,10 @@ let SessionStoreInternal = { this._initPrefs(); this._initialized = true; + // this pref is only read at startup, so no need to observe it + this._sessionhistory_max_entries = + this._prefBranch.getIntPref("sessionhistory.max_entries"); + // Wait until nsISessionStartup has finished reading the session data. gSessionStartup.onceInitialized.then(() => { // Parse session data and start restoring. @@ -1084,9 +1079,8 @@ let SessionStoreInternal = { // does a session history entry contain a url for the given domain? function containsDomain(aEntry) { try { - if (makeURI(aEntry.url).host.hasRootDomain(aData)) { + 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); @@ -1235,7 +1229,7 @@ let SessionStoreInternal = { } // Get the latest data for this tab (generally, from the cache) - let tabState = TabState.collectSync(aTab); + let tabState = this._collectTabData(aTab); // store closed-tab data for undo if (this._shouldSaveTabState(tabState)) { @@ -1426,7 +1420,7 @@ let SessionStoreInternal = { if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); - let tabState = TabState.collectSync(aTab); + let tabState = this._collectTabData(aTab); return this._toJSONString(tabState); }, @@ -1471,7 +1465,7 @@ let SessionStoreInternal = { throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); // Duplicate the tab state - let tabState = TabState.clone(aTab); + let tabState = this._cloneFullTabData(aTab); tabState.index += aDelta; tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); @@ -1887,69 +1881,447 @@ let SessionStoreInternal = { return [true, canOverwriteTabs]; }, - /* ........ Async Data Collection .............. */ + /* ........ Saving Functionality .............. */ /** - * Kicks off asynchronous data collection for all tabs that do not have any - * cached data. The returned promise will only notify that the tab collection - * has been finished without resolving to any data. The tab collection for a - * a few or all tabs might have failed or timed out. By calling - * fillTabCachesAsynchronously() and waiting for the promise to be resolved - * before calling getCurrentState(), callers ensure that most of the data - * should have been collected asynchronously, without blocking the main - * thread. + * Collect data related to a single tab * - * @return {Promise} the promise that is fulfilled when the tab data is ready + * @param aTab + * tabbrowser tab + * + * @returns {TabData} An object with the data for this tab. If the + * tab has not been invalidated since the last call to + * _collectTabData(aTab), the same object is returned. */ - fillTabCachesAsynchronously: function () { - let countdown = 0; - let deferred = Promise.defer(); - let activeWindow = this._getMostRecentBrowserWindow(); - - // The callback that will be called when a promise has been resolved - // successfully, i.e. the tab data has been collected. - function done() { - if (--countdown === 0) { - deferred.resolve(); - } + _collectTabData: function ssi_collectTabData(aTab) { + if (!aTab) { + throw new TypeError("Expecting a tab"); } - - // The callback that will be called when a promise is rejected, i.e. we - // we couldn't collect the tab data because of a script error or a timeout. - function fail(reason) { - debug("Failed collecting tab data asynchronously: " + reason); - done(); + let tabData; + if ((tabData = TabStateCache.get(aTab))) { + return tabData; } - - this._forEachBrowserWindow(win => { - if (!this._isWindowLoaded(win)) { - // Bail out if the window hasn't even loaded, yet. - return; - } - - if (!DirtyWindows.has(win) && win != activeWindow) { - // Bail out if the window is not dirty and inactive. - return; - } - - for (let tab of win.gBrowser.tabs) { - if (!TabStateCache.has(tab)) { - countdown++; - TabState.collect(tab).then(done, fail); - } - } - }); - - // If no dirty tabs were found, return a resolved - // promise because there is nothing to do here. - if (countdown == 0) { - return Promise.resolve(); + tabData = new TabData(this._collectBaseTabData(aTab)); + if (this._updateTextAndScrollDataForTab(aTab, tabData)) { + TabStateCache.set(aTab, tabData); } - - return deferred.promise; + return tabData; }, - /* ........ Saving Functionality .............. */ + /** + * Collect data related to a single tab, including private data. + * Use with caution. + * + * @param aTab + * tabbrowser tab + * + * @returns {object} An object with the data for this tab. This object + * is recomputed at every call. + */ + _cloneFullTabData: function ssi_cloneFullTabData(aTab) { + let options = { includePrivateData: true }; + let tabData = this._collectBaseTabData(aTab, options); + this._updateTextAndScrollDataForTab(aTab, tabData, options); + return tabData; + }, + + _collectBaseTabData: function ssi_collectBaseTabData(aTab, aOptions = null) { + let includePrivateData = aOptions && aOptions.includePrivateData; + let tabData = {entries: [], lastAccessed: aTab.lastAccessed }; + let browser = aTab.linkedBrowser; + if (!browser || !browser.currentURI) { + // can happen when calling this function right after .addTab() + return tabData; + } + 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 + + // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse + // data even when we shouldn't (e.g. Back, different anchor) + if (history && browser.__SS_data && + browser.__SS_data.entries[history.index] && + browser.__SS_data.entries[history.index].url == browser.currentURI.spec && + history.index < this._sessionhistory_max_entries - 1 && !includePrivateData) { + tabData = browser.__SS_data; + tabData.index = history.index + 1; + } + else if (history && history.count > 0) { + try { + for (var j = 0; j < history.count; j++) { + let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false), + includePrivateData, aTab.pinned); + tabData.entries.push(entry); + } + // If we make it through the for loop, then we're ok and we should clear + // any indicator of brokenness. + delete aTab.__SS_broken_history; + } + catch (ex) { + // In some cases, getEntryAtIndex will throw. This seems to be due to + // history.count being higher than it should be. By doing this in a + // try-catch, we'll update history to where it breaks, assert for + // non-release builds, and still save sessionstore.js. We'll track if + // we've shown the assert for this tab so we only show it once. + // cf. bug 669196. + if (!aTab.__SS_broken_history) { + // First Focus the window & tab we're having trouble with. + aTab.ownerDocument.defaultView.focus(); + aTab.ownerDocument.defaultView.gBrowser.selectedTab = aTab; + NS_ASSERT(false, "SessionStore failed gathering complete history " + + "for the focused window/tab. See bug 669196."); + aTab.__SS_broken_history = true; + } + } + tabData.index = history.index + 1; + + // make sure not to cache privacy sensitive data which shouldn't get out + if (!includePrivateData) + 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; + tabData.userTypedClear = browser.userTypedClear; + } 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, includePrivateData) + 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 aIncludePrivateData + * always return privacy sensitive data (use with care) + * @param aIsPinned + * the tab is pinned and should be treated differently for privacy + * @returns object + */ + _serializeHistoryEntry: + function ssi_serializeHistoryEntry(aEntry, aIncludePrivateData, aIsPinned) { + var entry = { url: aEntry.URI.spec }; + + 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 && (aIncludePrivateData || 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 (aIncludePrivateData || 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.owner) { + // 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.owner, Ci.nsISupports, 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 ownerBytes = + 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.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes)); + } + 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, aIncludePrivateData, + aIsPinned)); + } + } + + if (children.length) + entry.children = children; + } + + return entry; + }, + + /** + * Go through all frames and store the current scroll positions + * and innerHTML content of WYSIWYG editors + * + * @param aTab + * tabbrowser tab + * @param aTabData + * tabData object to add the information to + * @param options + * An optional object that may contain the following field: + * - includePrivateData: always return privacy sensitive data + * (use with care) + * @return false if data should not be cached because the tab + * has not been fully initialized yet. + */ + _updateTextAndScrollDataForTab: + function ssi_updateTextAndScrollDataForTab(aTab, aTabData, aOptions = null) { + let includePrivateData = aOptions && aOptions.includePrivateData; + let window = aTab.ownerDocument.defaultView; + let browser = aTab.linkedBrowser; + // we shouldn't update data for incompletely initialized tabs + if (!browser.currentURI + || (browser.__SS_data && browser.__SS_tabStillLoading)) { + return false; + } + + let 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 false; + } + + let selectedPageStyle = browser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : + this._getSelectedPageStyle(browser.contentWindow); + if (selectedPageStyle) + aTabData.pageStyle = selectedPageStyle; + else if (aTabData.pageStyle) + delete aTabData.pageStyle; + + this._updateTextAndScrollDataForFrame(window, browser.contentWindow, + aTabData.entries[tabIndex], + includePrivateData, + !!aTabData.pinned); + if (browser.currentURI.spec == "about:config") + aTabData.entries[tabIndex].formdata = { + id: { + "textbox": browser.contentDocument.getElementById("textbox").value + }, + xpath: {} + }; + return true; + }, + + /** + * 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 aIncludePrivateData + * always return privacy sensitive data (use with care) + * @param aIsPinned + * the tab is pinned and should be treated differently for privacy + */ + _updateTextAndScrollDataForFrame: + function ssi_updateTextAndScrollDataForFrame(aWindow, aContent, aData, + aIncludePrivateData, 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], + aIncludePrivateData, aIsPinned); + } + var isHTTPS = this._getURIFromString((aContent.parent || aContent). + document.location.href).schemeIs("https"); + let topURL = aContent.top.document.location.href; + let isAboutSR = topURL == "about:sessionrestore" || topURL == "about:welcomeback"; + if (aIncludePrivateData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) { + 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 ssi_getSelectedPageStyle(aContent) { + const forScreen = /(?:^|,)\s*(?:all|screen)\s*(?:,|$)/i; + for (let i = 0; i < aContent.document.styleSheets.length; i++) { + let ss = aContent.document.styleSheets[i]; + let media = ss.media.mediaText; + if (!ss.disabled && ss.title && (!media || forScreen.test(media))) + return ss.title + } + for (let i = 0; i < aContent.frames.length; i++) { + let selectedPageStyle = this._getSelectedPageStyle(aContent.frames[i]); + if (selectedPageStyle) + return selectedPageStyle; + } + return ""; + }, /** * Store window dimensions, visibility, sidebar @@ -2117,7 +2489,7 @@ let SessionStoreInternal = { // update the internal state data for this window for (let tab of tabs) { - tabsData.push(TabState.collectSync(tab)); + tabsData.push(this._collectTabData(tab)); } winData.selected = tabbrowser.mTabBox.selectedIndex + 1; @@ -2533,9 +2905,8 @@ let SessionStoreInternal = { // 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(makeURI(uri)); - } + if (uri) + browser.docShell.setCurrentURI(this._getURIFromString(uri)); // If the page has a title, set it. if (activePageData) { @@ -2709,7 +3080,7 @@ let SessionStoreInternal = { // 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(makeURI("about:blank")); + 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 @@ -2802,7 +3173,7 @@ let SessionStoreInternal = { var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. createInstance(Ci.nsISHEntry); - shEntry.setURI(makeURI(aEntry.url)); + shEntry.setURI(this._getURIFromString(aEntry.url)); shEntry.setTitle(aEntry.title || aEntry.url); if (aEntry.subframe) shEntry.setIsSubFrame(aEntry.subframe || false); @@ -2810,7 +3181,7 @@ let SessionStoreInternal = { if (aEntry.contentType) shEntry.contentType = aEntry.contentType; if (aEntry.referrer) - shEntry.referrerURI = makeURI(aEntry.referrer); + shEntry.referrerURI = this._getURIFromString(aEntry.referrer); if (aEntry.isSrcdocEntry) shEntry.srcdocData = aEntry.srcdocData; @@ -3337,6 +3708,25 @@ let SessionStoreInternal = { 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 ssi_checkPrivacyLevel(aIsHTTPS, aUseDefaultPref) { + let pref = "sessionstore.privacy_level"; + // If we're in the process of quitting and we're not autoresuming the session + // then we should treat it as a deferred session. We have a different privacy + // pref for that case. + if (!aUseDefaultPref && this._loadState == STATE_QUITTING && !this._doResumeSession()) + pref = "sessionstore.privacy_level_deferred"; + return this._prefBranch.getIntPref(pref) < (aIsHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL); + }, + /** * on popup windows, the XULWindow's attributes seem not to be set correctly * we use thus JSDOMWindow attributes for sizemode and normal window attributes @@ -4191,17 +4581,6 @@ function TabData(obj = null) { let TabStateCache = { _data: new WeakMap(), - /** - * Tells whether an entry is in the cache. - * - * @param {XULElement} aKey The tab or the associated browser. - * @return {bool} Whether there's a cached entry for the given tab. - */ - has: function (aTab) { - let key = this._normalizeToBrowser(aTab); - return this._data.has(key); - }, - /** * Add or replace an entry in the cache. * @@ -4298,402 +4677,6 @@ let TabStateCache = { } }; -/** - * Module that contains tab state collection methods. - */ -let TabState = { - // A map (xul:tab -> promise) that keeps track of tabs and - // their promises when collecting tab data asynchronously. - _pendingCollections: new WeakMap(), - - /** - * Collect data related to a single tab, asynchronously. - * - * @param tab - * tabbrowser tab - * - * @returns {Promise} A promise that will resolve to a TabData instance. - */ - collect: function (tab) { - if (!tab) { - throw new TypeError("Expecting a tab"); - } - - // Don't collect if we don't need to. - if (TabStateCache.has(tab)) { - return Promise.resolve(TabStateCache.get(tab)); - } - - let promise = Task.spawn(function task() { - // Collect basic tab data, without session history and storage. - let options = {omitSessionHistory: true, omitSessionStorage: true}; - let tabData = new TabData(this._collectBaseTabData(tab, options)); - - // Collected session history data asynchronously. - let history = yield Messenger.send(tab, "SessionStore:collectSessionHistory"); - tabData.entries = history.entries; - tabData.index = history.index; - - // Collected session storage data asynchronously. - let storage = yield Messenger.send(tab, "SessionStore:collectSessionStorage"); - if (Object.keys(storage).length) { - tabData.storage = storage; - } - - // Put tabData into cache when reading scroll and text data succeeds. - if (this._updateTextAndScrollDataForTab(tab, tabData)) { - // If we're still the latest async collection for the given tab and - // the cache hasn't been filled by collect() in the meantime, let's - // fill the cache with the data we received. - if (this._pendingCollections.get(tab) == promise) { - TabStateCache.set(tab, tabData); - this._pendingCollections.delete(tab); - } - } - - throw new Task.Result(tabData); - }.bind(this)); - - // Save the current promise as the latest asynchronous collection that is - // running. This will be used to check whether the collected data is still - // valid and will be used to fill the tab state cache. - this._pendingCollections.set(tab, promise); - - return promise; - }, - - /** - * Collect data related to a single tab, synchronously. - * - * @param tab - * tabbrowser tab - * - * @returns {TabData} An object with the data for this tab. If the - * tab has not been invalidated since the last call to - * collectSync(aTab), the same object is returned. - */ - collectSync: function (tab) { - if (!tab) { - throw new TypeError("Expecting a tab"); - } - if (TabStateCache.has(tab)) { - return TabStateCache.get(tab); - } - - let tabData = new TabData(this._collectBaseTabData(tab)); - if (this._updateTextAndScrollDataForTab(tab, tabData)) { - TabStateCache.set(tab, tabData); - } - - // Prevent all running asynchronous collections from filling the cache. - // Every asynchronous data collection started before a collectSync() call - // can't expect to retrieve different data than the sync call. That's why - // we just fill the cache with the data collected from the sync call and - // discard any data collected asynchronously. - this._pendingCollections.delete(tab); - - return tabData; - }, - - /** - * Collect data related to a single tab, including private data. - * Use with caution. - * - * @param tab - * tabbrowser tab - * - * @returns {object} An object with the data for this tab. This data is never - * cached, it will always be read from the tab and thus be - * up-to-date. - */ - clone: function (tab) { - let options = { includePrivateData: true }; - let tabData = this._collectBaseTabData(tab, options); - this._updateTextAndScrollDataForTab(tab, tabData, options); - return tabData; - }, - - /** - * Collects basic tab data for a given tab. - * - * @param tab - * tabbrowser tab - * @param options - * An object that will be passed to session history and session - * storage data collection methods. - * {omitSessionHistory: true} to skip collecting session history data - * {omitSessionStorage: true} to skip collecting session storage data - * - * The omit* options have been introduced to enable us collecting - * those parts of the tab data asynchronously. We will request basic - * tabData without the parts to omit and fill those holes later when - * the content script has responded. - * - * @returns {object} An object with the basic data for this tab. - */ - _collectBaseTabData: function (tab, options = {}) { - let tabData = {entries: [], lastAccessed: tab.lastAccessed }; - let browser = tab.linkedBrowser; - if (!browser || !browser.currentURI) { - // can happen when calling this function right after .addTab() - return tabData; - } - 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 (tab.pinned) - tabData.pinned = true; - else - delete tabData.pinned; - tabData.hidden = tab.hidden; - - // If __SS_extdata is set then we'll use that since it might be newer. - if (tab.__SS_extdata) - tabData.extData = tab.__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; - } - - // Collection session history data. - if (!options || !options.omitSessionHistory) { - this._collectTabHistory(tab, tabData, options); - } - - // If there is a userTypedValue set, then either the user has typed something - // in the URL bar, or a new tab was opened with a URI to load. userTypedClear - // is used to indicate whether the tab was in some sort of loading state with - // userTypedValue. - if (browser.userTypedValue) { - tabData.userTypedValue = browser.userTypedValue; - tabData.userTypedClear = browser.userTypedClear; - } else { - delete tabData.userTypedValue; - delete tabData.userTypedClear; - } - - if (tab.pinned) - tabData.pinned = true; - else - delete tabData.pinned; - tabData.hidden = tab.hidden; - - let 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(tab); - - // Store the tab icon. - let tabbrowser = tab.ownerDocument.defaultView.gBrowser; - tabData.image = tabbrowser.getIcon(tab); - - if (tab.__SS_extdata) - tabData.extData = tab.__SS_extdata; - else if (tabData.extData) - delete tabData.extData; - - // Collect DOMSessionStorage data. - if (!options || !options.omitSessionStorage) { - this._collectTabSessionStorage(tab, tabData, options); - } - - return tabData; - }, - - /** - * Collects session history data for a given tab. - * - * @param tab - * tabbrowser tab - * @param tabData - * An object that the session history data will be added to. - * @param options - * {includePrivateData: true} to always include private data - */ - _collectTabHistory: function (tab, tabData, options = {}) { - let includePrivateData = options && options.includePrivateData; - let docShell = tab.linkedBrowser.docShell; - - if (docShell instanceof Ci.nsIDocShell) { - let history = SessionHistory.read(docShell, includePrivateData); - tabData.entries = history.entries; - - // For blank tabs without any history entries, - // there will not be an 'index' property. - if ("index" in history) { - tabData.index = history.index; - } - } - }, - - /** - * Collects session history data for a given tab. - * - * @param tab - * tabbrowser tab - * @param tabData - * An object that the session storage data will be added to. - * @param options - * {includePrivateData: true} to always include private data - */ - _collectTabSessionStorage: function (tab, tabData, options = {}) { - let includePrivateData = options && options.includePrivateData; - let docShell = tab.linkedBrowser.docShell; - - if (docShell instanceof Ci.nsIDocShell) { - let storageData = SessionStorage.serialize(docShell, includePrivateData) - if (Object.keys(storageData).length) { - tabData.storage = storageData; - } - } - }, - - /** - * Go through all frames and store the current scroll positions - * and innerHTML content of WYSIWYG editors - * - * @param tab - * tabbrowser tab - * @param tabData - * tabData object to add the information to - * @param options - * An optional object that may contain the following field: - * - includePrivateData: always return privacy sensitive data - * (use with care) - * @return false if data should not be cached because the tab - * has not been fully initialized yet. - */ - _updateTextAndScrollDataForTab: function (tab, tabData, options = null) { - let includePrivateData = options && options.includePrivateData; - let window = tab.ownerDocument.defaultView; - let browser = tab.linkedBrowser; - // we shouldn't update data for incompletely initialized tabs - if (!browser.currentURI - || (browser.__SS_data && browser.__SS_tabStillLoading)) { - return false; - } - - let tabIndex = (tabData.index || tabData.entries.length) - 1; - // entry data needn't exist for tabs just initialized with an incomplete session state - if (!tabData.entries[tabIndex]) { - return false; - } - - let selectedPageStyle = browser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : - this._getSelectedPageStyle(browser.contentWindow); - if (selectedPageStyle) - tabData.pageStyle = selectedPageStyle; - else if (tabData.pageStyle) - delete tabData.pageStyle; - - this._updateTextAndScrollDataForFrame(window, browser.contentWindow, - tabData.entries[tabIndex], - includePrivateData, - !!tabData.pinned); - if (browser.currentURI.spec == "about:config") { - tabData.entries[tabIndex].formdata = { - id: { - "textbox": browser.contentDocument.getElementById("textbox").value - }, - xpath: {} - }; - } - - return true; - }, - - /** - * Go through all subframes and store all form data, the current - * scroll positions and innerHTML content of WYSIWYG editors. - * - * @param window - * Window reference - * @param content - * frame reference - * @param data - * part of a tabData object to add the information to - * @param includePrivateData - * always return privacy sensitive data (use with care) - * @param isPinned - * the tab is pinned and should be treated differently for privacy - */ - _updateTextAndScrollDataForFrame: - function (window, content, data, includePrivateData, isPinned) { - - for (let i = 0; i < content.frames.length; i++) { - if (data.children && data.children[i]) - this._updateTextAndScrollDataForFrame(window, content.frames[i], - data.children[i], - includePrivateData, isPinned); - } - let href = (content.parent || content).document.location.href; - let isHttps = makeURI(href).schemeIs("https"); - let topURL = content.top.document.location.href; - let isAboutSR = topURL == "about:sessionrestore" || topURL == "about:welcomeback"; - if (includePrivateData || isAboutSR || - PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) { - let formData = DocumentUtils.getFormData(content.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) { - data.formdata = formData; - } else if (data.formdata) { - delete data.formdata; - } - - // designMode is undefined e.g. for XUL documents (as about:config) - if ((content.document.designMode || "") == "on" && content.document.body) - data.innerHTML = content.document.body.innerHTML; - } - - // get scroll position from nsIDOMWindowUtils, since it allows avoiding a - // flush of layout - let domWindowUtils = content.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - let scrollX = {}, scrollY = {}; - domWindowUtils.getScrollXY(false, scrollX, scrollY); - data.scroll = scrollX.value + "," + scrollY.value; - }, - - /** - * determine the title of the currently enabled style sheet (if any) - * and recurse through the frameset if necessary - * @param content is a frame reference - * @returns the title style sheet determined to be enabled (empty string if none) - */ - _getSelectedPageStyle: function (content) { - const forScreen = /(?:^|,)\s*(?:all|screen)\s*(?:,|$)/i; - for (let i = 0; i < content.document.styleSheets.length; i++) { - let ss = content.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 < content.frames.length; i++) { - let selectedPageStyle = this._getSelectedPageStyle(content.frames[i]); - if (selectedPageStyle) - return selectedPageStyle; - } - return ""; - } -}; - let TabStateCacheTelemetry = { // Total number of hits during the session _hits: 0, diff --git a/browser/components/sessionstore/src/moz.build b/browser/components/sessionstore/src/moz.build index 4c5fe85ef08..c99282262cd 100644 --- a/browser/components/sessionstore/src/moz.build +++ b/browser/components/sessionstore/src/moz.build @@ -14,10 +14,7 @@ JS_MODULES_PATH = 'modules/sessionstore' EXTRA_JS_MODULES = [ 'DocumentUtils.jsm', - 'Messenger.jsm', - 'PrivacyLevel.jsm', 'SessionCookies.jsm', - 'SessionHistory.jsm', 'SessionMigration.jsm', 'SessionStorage.jsm', 'SessionWorker.js', diff --git a/browser/components/sessionstore/test/browser_625257.js b/browser/components/sessionstore/test/browser_625257.js index 91c4772fd0b..394fbd7cd38 100644 --- a/browser/components/sessionstore/test/browser_625257.js +++ b/browser/components/sessionstore/test/browser_625257.js @@ -58,6 +58,7 @@ function test() { ss.getBrowserState(); is(gBrowser.tabs[1], tab, "newly created tab should exist by now"); + ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state"); // Start a load and interrupt it by closing the tab tab.linkedBrowser.loadURI(URI_TO_LOAD);