michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["TabState"]; michael@0: michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); michael@0: Cu.import("resource://gre/modules/Promise.jsm", this); michael@0: Cu.import("resource://gre/modules/Task.jsm", this); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "console", michael@0: "resource://gre/modules/devtools/Console.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter", michael@0: "resource:///modules/sessionstore/PrivacyFilter.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache", michael@0: "resource:///modules/sessionstore/TabStateCache.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes", michael@0: "resource:///modules/sessionstore/TabAttributes.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Utils", michael@0: "resource:///modules/sessionstore/Utils.jsm"); michael@0: michael@0: /** michael@0: * Module that contains tab state collection methods. michael@0: */ michael@0: this.TabState = Object.freeze({ michael@0: setSyncHandler: function (browser, handler) { michael@0: TabStateInternal.setSyncHandler(browser, handler); michael@0: }, michael@0: michael@0: update: function (browser, data) { michael@0: TabStateInternal.update(browser, data); michael@0: }, michael@0: michael@0: flush: function (browser) { michael@0: TabStateInternal.flush(browser); michael@0: }, michael@0: michael@0: flushWindow: function (window) { michael@0: TabStateInternal.flushWindow(window); michael@0: }, michael@0: michael@0: collect: function (tab) { michael@0: return TabStateInternal.collect(tab); michael@0: }, michael@0: michael@0: clone: function (tab) { michael@0: return TabStateInternal.clone(tab); michael@0: } michael@0: }); michael@0: michael@0: let TabStateInternal = { michael@0: // A map (xul:browser -> handler) that maps a tab to the michael@0: // synchronous collection handler object for that tab. michael@0: // See SyncHandler in content-sessionStore.js. michael@0: _syncHandlers: new WeakMap(), michael@0: michael@0: // A map (xul:browser -> int) that maps a browser to the michael@0: // last "SessionStore:update" message ID we received for it. michael@0: _latestMessageID: new WeakMap(), michael@0: michael@0: /** michael@0: * Install the sync handler object from a given tab. michael@0: */ michael@0: setSyncHandler: function (browser, handler) { michael@0: this._syncHandlers.set(browser.permanentKey, handler); michael@0: this._latestMessageID.set(browser.permanentKey, 0); michael@0: }, michael@0: michael@0: /** michael@0: * Processes a data update sent by the content script. michael@0: */ michael@0: update: function (browser, {id, data}) { michael@0: // Only ever process messages that have an ID higher than the last one we michael@0: // saw. This ensures we don't use stale data that has already been received michael@0: // synchronously. michael@0: if (id > this._latestMessageID.get(browser.permanentKey)) { michael@0: this._latestMessageID.set(browser.permanentKey, id); michael@0: TabStateCache.update(browser, data); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Flushes all data currently queued in the given browser's content script. michael@0: */ michael@0: flush: function (browser) { michael@0: if (this._syncHandlers.has(browser.permanentKey)) { michael@0: let lastID = this._latestMessageID.get(browser.permanentKey); michael@0: this._syncHandlers.get(browser.permanentKey).flush(lastID); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Flushes queued content script data for all browsers of a given window. michael@0: */ michael@0: flushWindow: function (window) { michael@0: for (let browser of window.gBrowser.browsers) { michael@0: this.flush(browser); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Collect data related to a single tab, synchronously. michael@0: * michael@0: * @param tab michael@0: * tabbrowser tab michael@0: * michael@0: * @returns {TabData} An object with the data for this tab. If the michael@0: * tab has not been invalidated since the last call to michael@0: * collect(aTab), the same object is returned. michael@0: */ michael@0: collect: function (tab) { michael@0: return this._collectBaseTabData(tab); michael@0: }, michael@0: michael@0: /** michael@0: * Collect data related to a single tab, including private data. michael@0: * Use with caution. michael@0: * michael@0: * @param tab michael@0: * tabbrowser tab michael@0: * michael@0: * @returns {object} An object with the data for this tab. This data is never michael@0: * cached, it will always be read from the tab and thus be michael@0: * up-to-date. michael@0: */ michael@0: clone: function (tab) { michael@0: return this._collectBaseTabData(tab, {includePrivateData: true}); michael@0: }, michael@0: michael@0: /** michael@0: * Collects basic tab data for a given tab. michael@0: * michael@0: * @param tab michael@0: * tabbrowser tab michael@0: * @param options (object) michael@0: * {includePrivateData: true} to always include private data michael@0: * michael@0: * @returns {object} An object with the basic data for this tab. michael@0: */ michael@0: _collectBaseTabData: function (tab, options) { michael@0: let tabData = {entries: [], lastAccessed: tab.lastAccessed }; michael@0: let browser = tab.linkedBrowser; michael@0: michael@0: if (!browser || !browser.currentURI) { michael@0: // can happen when calling this function right after .addTab() michael@0: return tabData; michael@0: } michael@0: if (browser.__SS_data) { michael@0: // Use the data to be restored when the tab hasn't been michael@0: // completely loaded. We clone the data, since we're updating it michael@0: // here and the caller may update it further. michael@0: tabData = Utils.shallowCopy(browser.__SS_data); michael@0: if (tab.pinned) michael@0: tabData.pinned = true; michael@0: else michael@0: delete tabData.pinned; michael@0: tabData.hidden = tab.hidden; michael@0: michael@0: // If __SS_extdata is set then we'll use that since it might be newer. michael@0: if (tab.__SS_extdata) michael@0: tabData.extData = tab.__SS_extdata; michael@0: // If it exists but is empty then a key was likely deleted. In that case just michael@0: // delete extData. michael@0: if (tabData.extData && !Object.keys(tabData.extData).length) michael@0: delete tabData.extData; michael@0: return tabData; michael@0: } michael@0: michael@0: // If there is a userTypedValue set, then either the user has typed something michael@0: // in the URL bar, or a new tab was opened with a URI to load. userTypedClear michael@0: // is used to indicate whether the tab was in some sort of loading state with michael@0: // userTypedValue. michael@0: if (browser.userTypedValue) { michael@0: tabData.userTypedValue = browser.userTypedValue; michael@0: tabData.userTypedClear = browser.userTypedClear; michael@0: } else { michael@0: delete tabData.userTypedValue; michael@0: delete tabData.userTypedClear; michael@0: } michael@0: michael@0: if (tab.pinned) michael@0: tabData.pinned = true; michael@0: else michael@0: delete tabData.pinned; michael@0: tabData.hidden = tab.hidden; michael@0: michael@0: // Save tab attributes. michael@0: tabData.attributes = TabAttributes.get(tab); michael@0: michael@0: // Store the tab icon. michael@0: let tabbrowser = tab.ownerDocument.defaultView.gBrowser; michael@0: tabData.image = tabbrowser.getIcon(tab); michael@0: michael@0: if (tab.__SS_extdata) michael@0: tabData.extData = tab.__SS_extdata; michael@0: else if (tabData.extData) michael@0: delete tabData.extData; michael@0: michael@0: // Copy data from the tab state cache only if the tab has fully finished michael@0: // restoring. We don't want to overwrite data contained in __SS_data. michael@0: this._copyFromCache(tab, tabData, options); michael@0: michael@0: return tabData; michael@0: }, michael@0: michael@0: /** michael@0: * Copy tab data for the given |tab| from the cache to |tabData|. michael@0: * michael@0: * @param tab (xul:tab) michael@0: * The tab belonging to the given |tabData| object. michael@0: * @param tabData (object) michael@0: * The tab data belonging to the given |tab|. michael@0: * @param options (object) michael@0: * {includePrivateData: true} to always include private data michael@0: */ michael@0: _copyFromCache: function (tab, tabData, options = {}) { michael@0: let data = TabStateCache.get(tab.linkedBrowser); michael@0: if (!data) { michael@0: return; michael@0: } michael@0: michael@0: // The caller may explicitly request to omit privacy checks. michael@0: let includePrivateData = options && options.includePrivateData; michael@0: michael@0: for (let key of Object.keys(data)) { michael@0: let value = data[key]; michael@0: michael@0: // Filter sensitive data according to the current privacy level. michael@0: if (!includePrivateData) { michael@0: if (key === "storage") { michael@0: value = PrivacyFilter.filterSessionStorageData(value, tab.pinned); michael@0: } else if (key === "formdata") { michael@0: value = PrivacyFilter.filterFormData(value, tab.pinned); michael@0: } michael@0: } michael@0: michael@0: if (key === "history") { michael@0: tabData.entries = value.entries; michael@0: michael@0: if (value.hasOwnProperty("index")) { michael@0: tabData.index = value.index; michael@0: } michael@0: } else if (value) { michael@0: tabData[key] = value; michael@0: } michael@0: } michael@0: } michael@0: };