diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 541cd46cf118..93630d3f6a8e 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -858,6 +858,8 @@ pref("browser.sessionstore.debug", false); pref("browser.sessionstore.debug.no_auto_updates", false); // Forget closed windows/tabs after two weeks pref("browser.sessionstore.cleanup.forget_closed_after", 1209600000); +// Maximum number of bytes of DOMSessionStorage data we collect per origin. +pref("browser.sessionstore.dom_storage_limit", 2048); // Amount of failed SessionFile writes until we restart the worker. pref("browser.sessionstore.max_write_failures", 5); diff --git a/browser/base/content/test/trackingUI/browser_trackingUI_animation.js b/browser/base/content/test/trackingUI/browser_trackingUI_animation.js index 3d9362c42cb9..a9be39773cd7 100644 --- a/browser/base/content/test/trackingUI/browser_trackingUI_animation.js +++ b/browser/base/content/test/trackingUI/browser_trackingUI_animation.js @@ -7,14 +7,12 @@ const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/ const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html"; const TP_PREF = "privacy.trackingprotection.enabled"; const ANIMATIONS_PREF = "toolkit.cosmeticAnimations.enabled"; -const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications"; // Test that the shield icon animation can be controlled by the cosmetic // animations pref and that one of the icons is visible in each case. add_task(async function testShieldAnimation() { await UrlClassifierTestUtils.addTestTrackers(); Services.prefs.setBoolPref(TP_PREF, true); - Services.prefs.setBoolPref(DTSCBN_PREF, true); let tab = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); @@ -22,8 +20,7 @@ add_task(async function testShieldAnimation() { let noAnimationIcon = document.getElementById("tracking-protection-icon"); Services.prefs.setBoolPref(ANIMATIONS_PREF, true); - await Promise.all([promiseTabLoadEvent(tab, TRACKING_PAGE), - waitForContentBlockingEvent(2, tab.linkedBrowser.ownerGlobal)]); + await promiseTabLoadEvent(tab, TRACKING_PAGE); ok(BrowserTestUtils.is_hidden(noAnimationIcon), "the default icon is hidden when animations are enabled"); ok(BrowserTestUtils.is_visible(animationIcon), "the animated icon is shown when animations are enabled"); @@ -32,14 +29,12 @@ add_task(async function testShieldAnimation() { ok(BrowserTestUtils.is_hidden(noAnimationIcon), "the default icon is hidden"); Services.prefs.setBoolPref(ANIMATIONS_PREF, false); - await Promise.all([promiseTabLoadEvent(tab, TRACKING_PAGE), - waitForContentBlockingEvent(2, tab.linkedBrowser.ownerGlobal)]); + await promiseTabLoadEvent(tab, TRACKING_PAGE); ok(BrowserTestUtils.is_visible(noAnimationIcon), "the default icon is shown when animations are disabled"); ok(BrowserTestUtils.is_hidden(animationIcon), "the animated icon is hidden when animations are disabled"); gBrowser.removeCurrentTab(); Services.prefs.clearUserPref(ANIMATIONS_PREF); Services.prefs.clearUserPref(TP_PREF); - Services.prefs.clearUserPref(DTSCBN_PREF); UrlClassifierTestUtils.cleanupTestTrackers(); }); diff --git a/browser/base/content/test/trackingUI/browser_trackingUI_animation_2.js b/browser/base/content/test/trackingUI/browser_trackingUI_animation_2.js index b8f0897b8bff..f1d4446c1e28 100644 --- a/browser/base/content/test/trackingUI/browser_trackingUI_animation_2.js +++ b/browser/base/content/test/trackingUI/browser_trackingUI_animation_2.js @@ -11,7 +11,6 @@ const NCB_PREF = "network.cookie.cookieBehavior"; const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html"; const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html"; const COOKIE_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/cookiePage.html"; -const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications"; requestLongerTimeout(2); @@ -20,13 +19,10 @@ registerCleanupFunction(function() { Services.prefs.clearUserPref(TP_PREF); Services.prefs.clearUserPref(TP_PB_PREF); Services.prefs.clearUserPref(NCB_PREF); - Services.prefs.clearUserPref(DTSCBN_PREF); Services.prefs.clearUserPref(ContentBlocking.prefIntroCount); }); async function testTrackingProtectionAnimation(tabbrowser) { - Services.prefs.setBoolPref(DTSCBN_PREF, true); - info("Load a test page not containing tracking elements"); let benignTab = await BrowserTestUtils.openNewForegroundTab(tabbrowser, BENIGN_PAGE); let ContentBlocking = tabbrowser.ownerGlobal.ContentBlocking; diff --git a/browser/base/content/test/trackingUI/browser_trackingUI_pbmode_exceptions.js b/browser/base/content/test/trackingUI/browser_trackingUI_pbmode_exceptions.js index 15bc7b6beed9..c43bd70566fc 100644 --- a/browser/base/content/test/trackingUI/browser_trackingUI_pbmode_exceptions.js +++ b/browser/base/content/test/trackingUI/browser_trackingUI_pbmode_exceptions.js @@ -6,14 +6,12 @@ const TP_PB_PREF = "privacy.trackingprotection.enabled"; const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html"; -const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications"; var TrackingProtection = null; var ContentBlocking = null; var browser = null; registerCleanupFunction(function() { Services.prefs.clearUserPref(TP_PB_PREF); - Services.prefs.clearUserPref(DTSCBN_PREF); ContentBlocking = TrackingProtection = browser = null; UrlClassifierTestUtils.cleanupTestTrackers(); }); @@ -81,7 +79,6 @@ function testTrackingPageUnblocked() { add_task(async function testExceptionAddition() { await UrlClassifierTestUtils.addTestTrackers(); - Services.prefs.setBoolPref(DTSCBN_PREF, true); let privateWin = await BrowserTestUtils.openNewBrowserWindow({private: true}); browser = privateWin.gBrowser; let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser: browser, waitForLoad: true, waitForStateStop: true }); @@ -95,8 +92,7 @@ add_task(async function testExceptionAddition() { ok(TrackingProtection.enabled, "TP is enabled after setting the pref"); info("Load a test page containing tracking elements"); - await Promise.all([promiseTabLoadEvent(tab, TRACKING_PAGE), - waitForContentBlockingEvent(2, tab.ownerGlobal)]); + await promiseTabLoadEvent(tab, TRACKING_PAGE); testTrackingPage(tab.ownerGlobal); @@ -132,8 +128,7 @@ add_task(async function testExceptionPersistence() { ok(TrackingProtection.enabled, "TP is still enabled"); info("Load a test page containing tracking elements"); - await Promise.all([promiseTabLoadEvent(tab, TRACKING_PAGE), - waitForContentBlockingEvent(2, tab.ownerGlobal)]); + await promiseTabLoadEvent(tab, TRACKING_PAGE); testTrackingPage(tab.ownerGlobal); @@ -142,8 +137,7 @@ add_task(async function testExceptionPersistence() { clickButton("#tracking-action-unblock"); is(identityPopupState(), "closed", "Identity popup is closed"); - await Promise.all([tabReloadPromise, - waitForContentBlockingEvent(2, tab.ownerGlobal)]); + await tabReloadPromise; testTrackingPageUnblocked(); privateWin.close(); diff --git a/browser/base/content/test/trackingUI/browser_trackingUI_state.js b/browser/base/content/test/trackingUI/browser_trackingUI_state.js index 3c1081806356..e791c957f297 100644 --- a/browser/base/content/test/trackingUI/browser_trackingUI_state.js +++ b/browser/base/content/test/trackingUI/browser_trackingUI_state.js @@ -17,7 +17,6 @@ const TP_PREF = "privacy.trackingprotection.enabled"; const TP_PB_PREF = "privacy.trackingprotection.pbmode.enabled"; const TPC_PREF = "network.cookie.cookieBehavior"; -const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications"; const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html"; const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html"; const COOKIE_PAGE = "http://not-tracking.example.com/browser/browser/base/content/test/trackingUI/cookiePage.html"; @@ -34,7 +33,6 @@ registerCleanupFunction(function() { Services.prefs.clearUserPref(TP_PREF); Services.prefs.clearUserPref(TP_PB_PREF); Services.prefs.clearUserPref(TPC_PREF); - Services.prefs.clearUserPref(DTSCBN_PREF); }); // This is a special version of "hidden" that doesn't check for item @@ -108,8 +106,8 @@ function testTrackingPage(window) { ok(ContentBlocking.content.hasAttribute("detected"), "trackers are detected"); ok(!ContentBlocking.content.hasAttribute("hasException"), "content shows no exception"); - let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); - let blockedByTP = areTrackersBlocked(isWindowPrivate); + let isPrivateBrowsing = PrivateBrowsingUtils.isWindowPrivate(window); + let blockedByTP = areTrackersBlocked(isPrivateBrowsing); is(BrowserTestUtils.is_visible(ContentBlocking.iconBox), blockedByTP, "icon box is" + (blockedByTP ? "" : " not") + " visible"); is(ContentBlocking.iconBox.hasAttribute("active"), blockedByTP, @@ -121,6 +119,7 @@ function testTrackingPage(window) { ok(hidden("#tracking-action-block"), "blockButton is hidden"); + let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); if (isWindowPrivate) { ok(hidden("#tracking-action-unblock"), "unblockButton is hidden"); is(!hidden("#tracking-action-unblock-private"), blockedByTP, @@ -219,8 +218,6 @@ async function testContentBlocking(tab) { add_task(async function testNormalBrowsing() { await UrlClassifierTestUtils.addTestTrackers(); - Services.prefs.setBoolPref(DTSCBN_PREF, true); - tabbrowser = gBrowser; let tab = tabbrowser.selectedTab = BrowserTestUtils.addTab(tabbrowser); diff --git a/browser/base/content/test/trackingUI/browser_trackingUI_state_all_disabled.js b/browser/base/content/test/trackingUI/browser_trackingUI_state_all_disabled.js index 227fd7230bc2..49ea7846ab7d 100644 --- a/browser/base/content/test/trackingUI/browser_trackingUI_state_all_disabled.js +++ b/browser/base/content/test/trackingUI/browser_trackingUI_state_all_disabled.js @@ -8,7 +8,6 @@ const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/te const TP_PREF = "privacy.trackingprotection.enabled"; const COOKIE_PREF = "network.cookie.cookieBehavior"; const ANIMATIONS_PREF = "toolkit.cosmeticAnimations.enabled"; -const DTSCBN_PREF = "dom.testing.sync-content-blocking-notifications"; // Check that the shield icon is always hidden when all content blocking // categories are turned off, even when content blocking is on. @@ -16,7 +15,6 @@ add_task(async function testContentBlockingAllDisabled() { await SpecialPowers.pushPrefEnv({set: [ [TP_PREF, false], [COOKIE_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT], - [DTSCBN_PREF, true], ]}); await UrlClassifierTestUtils.addTestTrackers(); @@ -32,8 +30,7 @@ add_task(async function testContentBlockingAllDisabled() { let animationIcon = document.getElementById("tracking-protection-icon-animatable-image"); let noAnimationIcon = document.getElementById("tracking-protection-icon"); - await Promise.all([promiseTabLoadEvent(tab, TRACKING_PAGE), - waitForContentBlockingEvent(2, tab.ownerGlobal)]); + await promiseTabLoadEvent(tab, TRACKING_PAGE); ok(BrowserTestUtils.is_hidden(noAnimationIcon), "the default icon is hidden"); ok(BrowserTestUtils.is_hidden(animationIcon), "the animated icon is hidden"); @@ -50,7 +47,6 @@ add_task(async function testContentBlockingAllDisabled() { await SpecialPowers.pushPrefEnv({set: [ [TP_PREF, true], ]}); - await Promise.all([promiseTabLoadEvent(tab, TRACKING_PAGE), - waitForContentBlockingEvent(2, tab.ownerGlobal)]); + await promiseTabLoadEvent(tab, TRACKING_PAGE); ok(BrowserTestUtils.is_visible(noAnimationIcon), "the default icon is shown"); }); diff --git a/browser/components/sessionstore/ContentRestore.jsm b/browser/components/sessionstore/ContentRestore.jsm index 99064d0bfb5b..2872538663f0 100644 --- a/browser/components/sessionstore/ContentRestore.jsm +++ b/browser/components/sessionstore/ContentRestore.jsm @@ -8,8 +8,12 @@ var EXPORTED_SYMBOLS = ["ContentRestore"]; ChromeUtils.import("resource://gre/modules/Services.jsm", this); +ChromeUtils.defineModuleGetter(this, "FormData", + "resource://gre/modules/FormData.jsm"); ChromeUtils.defineModuleGetter(this, "SessionHistory", "resource://gre/modules/sessionstore/SessionHistory.jsm"); +ChromeUtils.defineModuleGetter(this, "SessionStorage", + "resource:///modules/sessionstore/SessionStorage.jsm"); ChromeUtils.defineModuleGetter(this, "Utils", "resource://gre/modules/sessionstore/Utils.jsm"); @@ -137,7 +141,7 @@ ContentRestoreInternal.prototype = { if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) { - SessionStoreUtils.restoreSessionStorage(this.docShell, tabData.storage); + SessionStorage.restore(this.docShell, tabData.storage); delete tabData.storage; } @@ -299,7 +303,7 @@ ContentRestoreInternal.prototype = { // restore() will return false, and thus abort restoration for the // current |frame| and its descendants, if |data.url| is given but // doesn't match the loaded document's URL. - return SessionStoreUtils.restoreFormData(frame.document, data); + return FormData.restore(frame, data); }); // Restore scroll data. diff --git a/browser/components/sessionstore/ContentSessionStore.jsm b/browser/components/sessionstore/ContentSessionStore.jsm index 5afd0c9e2e5a..836e89a2aebb 100644 --- a/browser/components/sessionstore/ContentSessionStore.jsm +++ b/browser/components/sessionstore/ContentSessionStore.jsm @@ -18,6 +18,8 @@ ChromeUtils.defineModuleGetter(this, "ContentRestore", "resource:///modules/sessionstore/ContentRestore.jsm"); ChromeUtils.defineModuleGetter(this, "SessionHistory", "resource://gre/modules/sessionstore/SessionHistory.jsm"); +ChromeUtils.defineModuleGetter(this, "SessionStorage", + "resource:///modules/sessionstore/SessionStorage.jsm"); ChromeUtils.defineModuleGetter(this, "Utils", "resource://gre/modules/sessionstore/Utils.jsm"); @@ -531,10 +533,7 @@ class SessionStorageListener extends Handler { // messages. this.resetChanges(); - this.messageQueue.push("storage", () => { - let data = SessionStoreUtils.collectSessionStorage(content); - return Object.keys(data).length ? data : null; - }); + this.messageQueue.push("storage", () => SessionStorage.collect(content)); } onPageLoadCompleted() { diff --git a/browser/components/sessionstore/SessionStorage.jsm b/browser/components/sessionstore/SessionStorage.jsm new file mode 100644 index 000000000000..e0710d149f1d --- /dev/null +++ b/browser/components/sessionstore/SessionStorage.jsm @@ -0,0 +1,211 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this file, +* You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["SessionStorage"]; + +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// A bound to the size of data to store for DOM Storage. +const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit"; + +// Returns the principal for a given |frame| contained in a given |docShell|. +function getPrincipalForFrame(docShell, frame) { + let ssm = Services.scriptSecurityManager; + let uri = frame.document.documentURIObject; + return ssm.getDocShellCodebasePrincipal(uri, docShell); +} + +var SessionStorage = Object.freeze({ + /** + * Updates all sessionStorage "super cookies" + * @param content + * A tab's global, i.e. the root frame we want to collect for. + * @return Returns a nested object that will have hosts as keys and per-origin + * session storage data as strings. For example: + * {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}} + */ + collect(content) { + return SessionStorageInternal.collect(content); + }, + + /** + * Restores all sessionStorage "super cookies". + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + * @param aStorageData + * A nested object with storage data to be restored that has hosts as + * keys and per-origin session storage data as strings. For example: + * {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}} + */ + restore(aDocShell, aStorageData) { + SessionStorageInternal.restore(aDocShell, aStorageData); + }, +}); + +/** + * Calls the given callback |cb|, passing |frame| and each of its descendants. + */ +function forEachNonDynamicChildFrame(frame, cb) { + // Call for current frame. + cb(frame); + + // Call the callback recursively for each descendant. + SessionStoreUtils.forEachNonDynamicChildFrame(frame, subframe => { + return forEachNonDynamicChildFrame(subframe, cb); + }); +} + +var SessionStorageInternal = { + /** + * Reads all session storage data from the given docShell. + * @param content + * A tab's global, i.e. the root frame we want to collect for. + * @return Returns a nested object that will have hosts as keys and per-origin + * session storage data as strings. For example: + * {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}} + */ + collect(content) { + let data = {}; + let visitedOrigins = new Set(); + let docShell = content.docShell; + + forEachNonDynamicChildFrame(content, frame => { + let principal = getPrincipalForFrame(docShell, frame); + if (!principal) { + return; + } + + // Get the origin of the current history entry + // and use that as a key for the per-principal storage data. + let origin; + try { + // The origin getter may throw for about:blank iframes as of bug 1340710, + // but we should ignore them anyway. + origin = principal.origin; + } catch (e) { + return; + } + if (visitedOrigins.has(origin)) { + // Don't read a host twice. + return; + } + + // Mark the current origin as visited. + visitedOrigins.add(origin); + + let originData = this._readEntry(principal, docShell); + if (Object.keys(originData).length) { + data[origin] = originData; + } + }); + + return Object.keys(data).length ? data : null; + }, + + /** + * Writes session storage data to the given tab. + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + * @param aStorageData + * A nested object with storage data to be restored that has hosts as + * keys and per-origin session storage data as strings. For example: + * {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}} + */ + restore(aDocShell, aStorageData) { + for (let origin of Object.keys(aStorageData)) { + let data = aStorageData[origin]; + + let principal; + + try { + // NOTE: In capture() we record the full origin for the URI which the + // sessionStorage is being captured for. As of bug 1235657 this code + // stopped parsing any origins which have originattributes correctly, as + // it decided to use the origin attributes from the docshell, and try to + // interpret the origin as a URI. Since bug 1353844 this code now correctly + // parses the full origin, and then discards the origin attributes, to + // make the behavior line up with the original intentions in bug 1235657 + // while preserving the ability to read all session storage from + // previous versions. In the future, if this behavior is desired, we may + // want to use the spec instead of the origin as the key, and avoid + // transmitting origin attribute information which we then discard when + // restoring. + // + // If changing this logic, make sure to also change the principal + // computation logic in SessionStore::_sendRestoreHistory. + let attrs = aDocShell.getOriginAttributes(); + let dataPrincipal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin); + principal = Services.scriptSecurityManager.createCodebasePrincipal(dataPrincipal.URI, attrs); + } catch (e) { + console.error(e); + continue; + } + + let storageManager = aDocShell.QueryInterface(Ci.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. + // Null window because the current window doesn't match the principal yet + // and loads about:blank. + let storage = storageManager.createStorage(null, principal, "", aDocShell.usePrivateBrowsing); + + for (let key of Object.keys(data)) { + try { + storage.setItem(key, data[key]); + } catch (e) { + // throws e.g. for URIs that can't have sessionStorage + console.error(e); + } + } + } + }, + + /** + * Reads an entry in the session storage data contained in a tab's history. + * @param aURI + * That history entry uri + * @param aDocShell + * A tab's docshell (containing the sessionStorage) + */ + _readEntry(aPrincipal, aDocShell) { + let hostData = {}; + let storage; + + let window = aDocShell.domWindow; + + try { + let storageManager = aDocShell.QueryInterface(Ci.nsIDOMStorageManager); + storage = storageManager.getStorage(window, aPrincipal); + storage.length; // XXX: Bug 1232955 - storage.length can throw, catch that failure + } catch (e) { + // sessionStorage might throw if it's turned off, see bug 458954 + storage = null; + } + + if (!storage || !storage.length) { + return hostData; + } + + // If the DOMSessionStorage contains too much data, ignore it. + let usage = window.windowUtils.getStorageUsage(storage); + if (usage > Services.prefs.getIntPref(DOM_STORAGE_LIMIT_PREF)) { + return hostData; + } + + for (let i = 0; i < storage.length; i++) { + try { + let key = storage.key(i); + hostData[key] = storage.getItem(key); + } catch (e) { + // This currently throws for secured items (cf. bug 442048). + } + } + + return hostData; + }, +}; diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build index 07006d8d06b2..512da69fa523 100644 --- a/browser/components/sessionstore/moz.build +++ b/browser/components/sessionstore/moz.build @@ -20,6 +20,7 @@ EXTRA_JS_MODULES.sessionstore = [ 'SessionMigration.jsm', 'SessionSaver.jsm', 'SessionStartup.jsm', + 'SessionStorage.jsm', 'SessionStore.jsm', 'SessionWorker.js', 'SessionWorker.jsm', diff --git a/dom/base/Document.h b/dom/base/Document.h index e8b6b8e14ded..1431da4cfa49 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -3575,8 +3575,6 @@ class Document : public nsINode, void SetDocTreeHadAudibleMedia(); void SetDocTreeHadPlayRevoked(); - mozilla::dom::XPathEvaluator* XPathEvaluator(); - protected: void DoUpdateSVGUseElementShadowTrees(); @@ -3706,6 +3704,8 @@ class Document : public nsINode, nsCString GetContentTypeInternal() const { return mContentType; } + mozilla::dom::XPathEvaluator* XPathEvaluator(); + // Update our frame request callback scheduling state, if needed. This will // schedule or unschedule them, if necessary, and update // mFrameRequestCallbacksScheduled. aOldShell should only be passed when diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp index 96cb359202fa..25019db8c3c5 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp @@ -1274,7 +1274,7 @@ void nsGlobalWindowInner::FreeInnerObjects(bool aForDocumentOpen) { } if (mWindowGlobalChild && !mWindowGlobalChild->IsClosed()) { - mWindowGlobalChild->Destroy(); + mWindowGlobalChild->Send__delete__(mWindowGlobalChild); } mWindowGlobalChild = nullptr; diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index b63e00c82ef1..8fd18a394c0d 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -319,8 +319,6 @@ static LazyLogModule gDOMLeakPRLogOuter("DOMLeakOuter"); static int32_t gOpenPopupSpamCount = 0; -static bool gSyncContentBlockingNotifications = false; - nsGlobalWindowOuter::OuterWindowByIdTable* nsGlobalWindowOuter::sOuterWindowsById = nullptr; @@ -5407,130 +5405,100 @@ void nsGlobalWindowOuter::NotifyContentBlockingEvent(unsigned aEvent, nsCOMPtr doc = docShell->GetDocument(); NS_ENSURE_TRUE_VOID(doc); - nsCOMPtr uri(aURIHint); - nsCOMPtr channel(aChannel); - - static bool prefInitialized = false; - if (!prefInitialized) { - Preferences::AddBoolVarCache( - &gSyncContentBlockingNotifications, - "dom.testing.sync-content-blocking-notifications", false); - prefInitialized = true; - } - - nsCOMPtr func = NS_NewRunnableFunction( - "NotifyContentBlockingEventDelayed", - [doc, docShell, uri, channel, aEvent, aBlocked]() { - // This event might come after the user has navigated to another - // page. To prevent showing the TrackingProtection UI on the wrong - // page, we need to check that the loading URI for the channel is - // the same as the URI currently loaded in the document. - if (!SameLoadingURI(doc, channel) && - aEvent == nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT) { - return; - } - - // Notify nsIWebProgressListeners of this content blocking event. - // Can be used to change the UI state. - nsresult rv = NS_OK; - nsCOMPtr eventSink = - do_QueryInterface(docShell, &rv); - NS_ENSURE_SUCCESS_VOID(rv); - uint32_t event = 0; - nsCOMPtr securityUI; - docShell->GetSecurityUI(getter_AddRefs(securityUI)); - if (!securityUI) { - return; - } - securityUI->GetContentBlockingEvent(&event); - nsAutoCString origin; - nsContentUtils::GetASCIIOrigin(uri, origin); - - bool blockedValue = aBlocked; - bool unblocked = false; - if (aEvent == nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT) { - doc->SetHasTrackingContentBlocked(aBlocked, origin); - if (!aBlocked) { - unblocked = !doc->GetHasTrackingContentBlocked(); - } - } else if (aEvent == - nsIWebProgressListener::STATE_LOADED_TRACKING_CONTENT) { - doc->SetHasTrackingContentLoaded(aBlocked, origin); - if (!aBlocked) { - unblocked = !doc->GetHasTrackingContentLoaded(); - } - } else if (aEvent == nsIWebProgressListener:: - STATE_COOKIES_BLOCKED_BY_PERMISSION) { - doc->SetHasCookiesBlockedByPermission(aBlocked, origin); - if (!aBlocked) { - unblocked = !doc->GetHasCookiesBlockedByPermission(); - } - } else if (aEvent == - nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) { - doc->SetHasTrackingCookiesBlocked(aBlocked, origin); - if (!aBlocked) { - unblocked = !doc->GetHasTrackingCookiesBlocked(); - } - } else if (aEvent == - nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL) { - doc->SetHasAllCookiesBlocked(aBlocked, origin); - if (!aBlocked) { - unblocked = !doc->GetHasAllCookiesBlocked(); - } - } else if (aEvent == - nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN) { - doc->SetHasForeignCookiesBlocked(aBlocked, origin); - if (!aBlocked) { - unblocked = !doc->GetHasForeignCookiesBlocked(); - } - } else if (aEvent == nsIWebProgressListener::STATE_COOKIES_LOADED) { - MOZ_ASSERT(!aBlocked, - "We don't expected to see blocked STATE_COOKIES_LOADED"); - // Note that the logic in this branch is the logical negation of - // the logic in other branches, since the Document API we have is - // phrased in "loaded" terms as opposed to "blocked" terms. - blockedValue = !aBlocked; - doc->SetHasCookiesLoaded(blockedValue, origin); - if (!aBlocked) { - unblocked = !doc->GetHasCookiesLoaded(); - } - } else { - // Ignore nsIWebProgressListener::STATE_BLOCKED_UNSAFE_CONTENT; - } - const uint32_t oldEvent = event; - if (blockedValue) { - event |= aEvent; - } else if (unblocked) { - event &= ~aEvent; - } - - if (event == oldEvent -#ifdef ANDROID - // GeckoView always needs to notify about blocked trackers, - // since the GeckoView API always needs to report the URI and - // type of any blocked tracker. We use a platform-dependent code - // path here because reporting this notification on desktop - // platforms isn't necessary and doing so can have a big - // performance cost. - && aEvent != nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT -#endif - ) { - // Avoid dispatching repeated notifications when nothing has - // changed - return; - } - - eventSink->OnContentBlockingEvent(channel, event); - }); - nsresult rv; - if (gSyncContentBlockingNotifications) { - rv = func->Run(); - } else { - rv = NS_IdleDispatchToCurrentThread(func.forget(), 100); - } - if (NS_WARN_IF(NS_FAILED(rv))) { + // This event might come after the user has navigated to another page. + // To prevent showing the TrackingProtection UI on the wrong page, we need to + // check that the loading URI for the channel is the same as the URI currently + // loaded in the document. + if (!SameLoadingURI(doc, aChannel) && + aEvent == nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT) { return; } + + // Notify nsIWebProgressListeners of this content blocking event. + // Can be used to change the UI state. + nsresult rv = NS_OK; + nsCOMPtr eventSink = do_QueryInterface(docShell, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + uint32_t event = 0; + nsCOMPtr securityUI; + docShell->GetSecurityUI(getter_AddRefs(securityUI)); + if (!securityUI) { + return; + } + securityUI->GetContentBlockingEvent(&event); + nsAutoCString origin; + nsContentUtils::GetASCIIOrigin(aURIHint, origin); + + bool blockedValue = aBlocked; + bool unblocked = false; + if (aEvent == nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT) { + doc->SetHasTrackingContentBlocked(aBlocked, origin); + if (!aBlocked) { + unblocked = !doc->GetHasTrackingContentBlocked(); + } + } else if (aEvent == nsIWebProgressListener::STATE_LOADED_TRACKING_CONTENT) { + doc->SetHasTrackingContentLoaded(aBlocked, origin); + if (!aBlocked) { + unblocked = !doc->GetHasTrackingContentLoaded(); + } + } else if (aEvent == + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION) { + doc->SetHasCookiesBlockedByPermission(aBlocked, origin); + if (!aBlocked) { + unblocked = !doc->GetHasCookiesBlockedByPermission(); + } + } else if (aEvent == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) { + doc->SetHasTrackingCookiesBlocked(aBlocked, origin); + if (!aBlocked) { + unblocked = !doc->GetHasTrackingCookiesBlocked(); + } + } else if (aEvent == nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL) { + doc->SetHasAllCookiesBlocked(aBlocked, origin); + if (!aBlocked) { + unblocked = !doc->GetHasAllCookiesBlocked(); + } + } else if (aEvent == nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN) { + doc->SetHasForeignCookiesBlocked(aBlocked, origin); + if (!aBlocked) { + unblocked = !doc->GetHasForeignCookiesBlocked(); + } + } else if (aEvent == nsIWebProgressListener::STATE_COOKIES_LOADED) { + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked STATE_COOKIES_LOADED"); + // Note that the logic in this branch is the logical negation of the logic + // in other branches, since the Document API we have is phrased in + // "loaded" terms as opposed to "blocked" terms. + blockedValue = !aBlocked; + doc->SetHasCookiesLoaded(blockedValue, origin); + if (!aBlocked) { + unblocked = !doc->GetHasCookiesLoaded(); + } + } else { + // Ignore nsIWebProgressListener::STATE_BLOCKED_UNSAFE_CONTENT; + } + const uint32_t oldEvent = event; + if (blockedValue) { + event |= aEvent; + } else if (unblocked) { + event &= ~aEvent; + } + + if (event == oldEvent +#ifdef ANDROID + // GeckoView always needs to notify about blocked trackers, since the + // GeckoView API always needs to report the URI and type of any blocked + // tracker. + // We use a platform-dependent code path here because reporting this + // notification on desktop platforms isn't necessary and doing so can have + // a big performance cost. + && aEvent != nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT +#endif + ) { + // Avoid dispatching repeated notifications when nothing has changed + return; + } + + eventSink->OnContentBlockingEvent(aChannel, event); } // static diff --git a/dom/chrome-webidl/SessionStoreUtils.webidl b/dom/chrome-webidl/SessionStoreUtils.webidl index 93b226ec6b15..9376ed246862 100644 --- a/dom/chrome-webidl/SessionStoreUtils.webidl +++ b/dom/chrome-webidl/SessionStoreUtils.webidl @@ -111,28 +111,6 @@ namespace SessionStoreUtils { * Form data encoded in an object. */ CollectedFormData collectFormData(Document document); - boolean restoreFormData(Document document, optional CollectedFormData data); - - /** - * Updates all sessionStorage "super cookies" - * @param content - * A tab's global, i.e. the root frame we want to collect for. - * @return Returns a nested object that will have hosts as keys and per-origin - * session storage data as strings. For example: - * {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}} - */ - record> collectSessionStorage(WindowProxy window); - - /** - * Restores all sessionStorage "super cookies". - * @param aDocShell - * A tab's docshell (containing the sessionStorage) - * @param aStorageData - * A nested object with storage data to be restored that has hosts as - * keys and per-origin session storage data as strings. For example: - * {"https://example.com^userContextId=1": {"key": "value", "my_number": "123"}} - */ - void restoreSessionStorage(nsIDocShell docShell, record> data); }; dictionary SSScrollPositionDict { @@ -152,7 +130,7 @@ dictionary CollectedNonMultipleSelectValue }; // object contains either a CollectedFileListValue or a CollectedNonMultipleSelectValue or Sequence -typedef (DOMString or boolean or object) CollectedFormDataValue; +typedef (DOMString or boolean or long or object) CollectedFormDataValue; dictionary CollectedFormData { diff --git a/dom/ipc/PWindowGlobal.ipdl b/dom/ipc/PWindowGlobal.ipdl index 9649bc086adc..161e49b70d7b 100644 --- a/dom/ipc/PWindowGlobal.ipdl +++ b/dom/ipc/PWindowGlobal.ipdl @@ -23,9 +23,6 @@ async protocol PWindowGlobal { manager PBrowser or PInProcess; -child: - async __delete__(); - parent: /// Update the URI of the document in this WindowGlobal. async UpdateDocumentURI(nsIURI aUri); @@ -33,7 +30,7 @@ parent: /// Notify the parent that this PWindowGlobal is now the current global. async BecomeCurrentWindowGlobal(); - async Destroy(); + async __delete__(); }; } // namespace dom diff --git a/dom/ipc/WindowGlobalChild.cpp b/dom/ipc/WindowGlobalChild.cpp index 0f296a754fbc..4b92acd64b07 100644 --- a/dom/ipc/WindowGlobalChild.cpp +++ b/dom/ipc/WindowGlobalChild.cpp @@ -90,24 +90,6 @@ already_AddRefed WindowGlobalChild::GetParentActor() { return do_AddRef(static_cast(otherSide)); } -already_AddRefed WindowGlobalChild::GetTabChild() { - if (IsInProcess() || mIPCClosed) { - return nullptr; - } - return do_AddRef(static_cast(Manager())); -} - -void WindowGlobalChild::Destroy() { - // Perform async IPC shutdown unless we're not in-process, and our TabChild is - // in the process of being destroyed, which will destroy us as well. - RefPtr tabChild = GetTabChild(); - if (!tabChild || !tabChild->IsDestroyed()) { - SendDestroy(); - } - - mIPCClosed = true; -} - void WindowGlobalChild::ActorDestroy(ActorDestroyReason aWhy) { mIPCClosed = true; gWindowGlobalChildById->Remove(mInnerWindowId); diff --git a/dom/ipc/WindowGlobalChild.h b/dom/ipc/WindowGlobalChild.h index 404e9bf59315..3218733104f2 100644 --- a/dom/ipc/WindowGlobalChild.h +++ b/dom/ipc/WindowGlobalChild.h @@ -42,7 +42,6 @@ class WindowGlobalChild : public nsWrapperCache, public PWindowGlobalChild { // Has this actor been shut down bool IsClosed() { return mIPCClosed; } - void Destroy(); // Check if this actor is managed by PInProcess, as-in the document is loaded // in the chrome process. @@ -58,10 +57,6 @@ class WindowGlobalChild : public nsWrapperCache, public PWindowGlobalChild { // |nullptr| if the actor has been torn down, or is not in-process. already_AddRefed GetParentActor(); - // Get this actor's manager if it is not an in-process actor. Returns - // |nullptr| if the actor has been torn down, or is in-process. - already_AddRefed GetTabChild(); - // Create and initialize the WindowGlobalChild object. static already_AddRefed Create( nsGlobalWindowInner* aWindow); diff --git a/dom/ipc/WindowGlobalParent.cpp b/dom/ipc/WindowGlobalParent.cpp index 235d69250e7d..1257e7244016 100644 --- a/dom/ipc/WindowGlobalParent.cpp +++ b/dom/ipc/WindowGlobalParent.cpp @@ -116,13 +116,6 @@ already_AddRefed WindowGlobalParent::GetChildActor() { return do_AddRef(static_cast(otherSide)); } -already_AddRefed WindowGlobalParent::GetTabParent() { - if (IsInProcess() || mIPCClosed) { - return nullptr; - } - return do_AddRef(static_cast(Manager())); -} - IPCResult WindowGlobalParent::RecvUpdateDocumentURI(nsIURI* aURI) { // XXX(nika): Assert that the URI change was one which makes sense (either // about:blank -> a real URI, or a legal push/popstate URI change?) @@ -135,16 +128,6 @@ IPCResult WindowGlobalParent::RecvBecomeCurrentWindowGlobal() { return IPC_OK(); } -IPCResult WindowGlobalParent::RecvDestroy() { - if (!mIPCClosed) { - RefPtr tabParent = GetTabParent(); - if (!tabParent || !tabParent->IsDestroyed()) { - Unused << Send__delete__(this); - } - } - return IPC_OK(); -} - bool WindowGlobalParent::IsCurrentGlobal() { return !mIPCClosed && mBrowsingContext->GetCurrentWindowGlobal() == this; } diff --git a/dom/ipc/WindowGlobalParent.h b/dom/ipc/WindowGlobalParent.h index 98aa9a1cc055..0d360dcd0747 100644 --- a/dom/ipc/WindowGlobalParent.h +++ b/dom/ipc/WindowGlobalParent.h @@ -51,10 +51,6 @@ class WindowGlobalParent final : public nsISupports, // |nullptr| if the actor has been torn down, or is not in-process. already_AddRefed GetChildActor(); - // Get this actor's manager if it is not an in-process actor. Returns - // |nullptr| if the actor has been torn down, or is in-process. - already_AddRefed GetTabParent(); - // The principal of this WindowGlobal. This value will not change over the // lifetime of the WindowGlobal object, even to reflect changes in // |document.domain|. @@ -94,7 +90,6 @@ class WindowGlobalParent final : public nsISupports, // IPC messages mozilla::ipc::IPCResult RecvUpdateDocumentURI(nsIURI* aURI) override; mozilla::ipc::IPCResult RecvBecomeCurrentWindowGlobal() override; - mozilla::ipc::IPCResult RecvDestroy() override; void ActorDestroy(ActorDestroyReason aWhy) override; diff --git a/dom/xslt/xpath/XPathEvaluator.h b/dom/xslt/xpath/XPathEvaluator.h index 7f82319a1ced..a12de5c636d2 100644 --- a/dom/xslt/xpath/XPathEvaluator.h +++ b/dom/xslt/xpath/XPathEvaluator.h @@ -48,9 +48,6 @@ class XPathEvaluator final : public NonRefcountedDOMObject { ErrorResult& rv); XPathExpression* CreateExpression(const nsAString& aExpression, nsINode* aResolver, ErrorResult& aRv); - XPathExpression* CreateExpression(const nsAString& aExpression, - txIParseContext* aContext, - Document* aDocument, ErrorResult& aRv); nsINode* CreateNSResolver(nsINode& aNodeResolver) { return &aNodeResolver; } already_AddRefed Evaluate( JSContext* aCx, const nsAString& aExpression, nsINode& aContextNode, @@ -58,6 +55,10 @@ class XPathEvaluator final : public NonRefcountedDOMObject { ErrorResult& rv); private: + XPathExpression* CreateExpression(const nsAString& aExpression, + txIParseContext* aContext, + Document* aDocument, ErrorResult& aRv); + nsWeakPtr mDocument; RefPtr mRecycler; }; diff --git a/dom/xslt/xpath/moz.build b/dom/xslt/xpath/moz.build index 664d3dbc1680..c053defaf532 100644 --- a/dom/xslt/xpath/moz.build +++ b/dom/xslt/xpath/moz.build @@ -5,7 +5,6 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXPORTS.mozilla.dom += [ - 'txIXPathContext.h', 'XPathEvaluator.h', 'XPathExpression.h', 'XPathResult.h', diff --git a/dom/xslt/xpath/txIXPathContext.h b/dom/xslt/xpath/txIXPathContext.h index 5a8156d60478..19a4b312df6e 100644 --- a/dom/xslt/xpath/txIXPathContext.h +++ b/dom/xslt/xpath/txIXPathContext.h @@ -6,9 +6,7 @@ #ifndef __TX_I_XPATH_CONTEXT #define __TX_I_XPATH_CONTEXT -#include "nscore.h" -#include "nsISupportsImpl.h" -#include "nsStringFwd.h" +#include "txCore.h" class FunctionCall; class nsAtom; diff --git a/mobile/android/chrome/geckoview/GeckoViewContentChild.js b/mobile/android/chrome/geckoview/GeckoViewContentChild.js index 756842000e99..9e9b0cd8ef0f 100644 --- a/mobile/android/chrome/geckoview/GeckoViewContentChild.js +++ b/mobile/android/chrome/geckoview/GeckoViewContentChild.js @@ -7,6 +7,7 @@ ChromeUtils.import("resource://gre/modules/GeckoViewChildModule.jsm"); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { + FormData: "resource://gre/modules/FormData.jsm", FormLikeFactory: "resource://gre/modules/FormLikeFactory.jsm", GeckoViewAutoFill: "resource://gre/modules/GeckoViewAutoFill.jsm", PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm", @@ -202,7 +203,7 @@ class GeckoViewContentChild extends GeckoViewChildModule { // restore() will return false, and thus abort restoration for the // current |frame| and its descendants, if |data.url| is given but // doesn't match the loaded document's URL. - return SessionStoreUtils.restoreFormData(frame.document, data); + return FormData.restore(frame, data); }); } }, {capture: true, mozSystemGroup: true, once: true}); diff --git a/mobile/android/components/SessionStore.js b/mobile/android/components/SessionStore.js index f2f375a2bd5f..2d14b25c4aff 100644 --- a/mobile/android/components/SessionStore.js +++ b/mobile/android/components/SessionStore.js @@ -9,6 +9,7 @@ ChromeUtils.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { EventDispatcher: "resource://gre/modules/Messaging.jsm", + FormData: "resource://gre/modules/FormData.jsm", OS: "resource://gre/modules/osfile.jsm", PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", @@ -1394,7 +1395,7 @@ SessionStore.prototype = { // restore() will return false, and thus abort restoration for the // current |frame| and its descendants, if |data.url| is given but // doesn't match the loaded document's URL. - return SessionStoreUtils.restoreFormData(frame.document, data); + return FormData.restore(frame, data); }); } }, diff --git a/modules/libpref/init/StaticPrefList.h b/modules/libpref/init/StaticPrefList.h index e0cead9356fe..059a91d9dc1c 100644 --- a/modules/libpref/init/StaticPrefList.h +++ b/modules/libpref/init/StaticPrefList.h @@ -1801,16 +1801,6 @@ VARCACHE_PREF( bool, false ) -//--------------------------------------------------------------------------- -// ContentSessionStore prefs -//--------------------------------------------------------------------------- -// Maximum number of bytes of DOMSessionStorage data we collect per origin. -VARCACHE_PREF( - "browser.sessionstore.dom_storage_limit", - browser_sessionstore_dom_storage_limit, - uint32_t, 2048 -) - //--------------------------------------------------------------------------- // Preferences prefs //--------------------------------------------------------------------------- diff --git a/toolkit/components/sessionstore/SessionStoreUtils.cpp b/toolkit/components/sessionstore/SessionStoreUtils.cpp index 0ae8b7c0c008..0928c3c6cad6 100644 --- a/toolkit/components/sessionstore/SessionStoreUtils.cpp +++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp @@ -3,16 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "js/JSON.h" -#include "jsapi.h" #include "mozilla/dom/HTMLInputElement.h" #include "mozilla/dom/HTMLSelectElement.h" #include "mozilla/dom/HTMLTextAreaElement.h" #include "mozilla/dom/SessionStoreUtils.h" -#include "mozilla/dom/txIXPathContext.h" #include "mozilla/dom/WindowProxyHolder.h" -#include "mozilla/dom/XPathResult.h" -#include "mozilla/dom/XPathEvaluator.h" -#include "mozilla/dom/XPathExpression.h" #include "nsCharSeparatedTokenizer.h" #include "nsContentList.h" #include "nsContentUtils.h" @@ -327,11 +322,11 @@ static bool IsValidCCNumber(nsAString& aValue) { static const uint16_t kMaxTraversedXPaths = 100; // A helper function to append a element into mId or mXpath of CollectedFormData -static Record::EntryType* +static Record::EntryType* AppendEntryToCollectedData(nsINode* aNode, const nsAString& aId, uint16_t& aGeneratedCount, CollectedFormData& aRetVal) { - Record::EntryType* entry; + Record::EntryType* entry; if (!aId.IsEmpty()) { if (!aRetVal.mId.WasPassed()) { aRetVal.mId.Construct(); @@ -391,7 +386,7 @@ static void CollectFromTextAreaElement(Document& aDocument, eCaseMatters)) { continue; } - Record::EntryType* entry = + Record::EntryType* entry = AppendEntryToCollectedData(textArea, id, aGeneratedCount, aRetVal); entry->mValue.SetAsString() = value; } @@ -446,7 +441,7 @@ static void CollectFromInputElement(JSContext* aCx, Document& aDocument, if (checked == input->DefaultChecked()) { continue; } - Record::EntryType* entry = + Record::EntryType* entry = AppendEntryToCollectedData(input, id, aGeneratedCount, aRetVal); entry->mValue.SetAsBoolean() = checked; } else if (input->ControlType() == NS_FORM_INPUT_FILE) { @@ -465,7 +460,7 @@ static void CollectFromInputElement(JSContext* aCx, Document& aDocument, JS_ClearPendingException(aCx); continue; } - Record::EntryType* entry = + Record::EntryType* entry = AppendEntryToCollectedData(input, id, aGeneratedCount, aRetVal); entry->mValue.SetAsObject() = &jsval.toObject(); } else { @@ -491,7 +486,7 @@ static void CollectFromInputElement(JSContext* aCx, Document& aDocument, JS::Rooted jsval(aCx); if (JS_ParseJSON(aCx, value.get(), value.Length(), &jsval) && jsval.isObject()) { - Record::EntryType* + Record::EntryType* entry = AppendEntryToCollectedData(input, id, aGeneratedCount, aRetVal); entry->mValue.SetAsObject() = &jsval.toObject(); @@ -502,7 +497,7 @@ static void CollectFromInputElement(JSContext* aCx, Document& aDocument, } } } - Record::EntryType* entry = + Record::EntryType* entry = AppendEntryToCollectedData(input, id, aGeneratedCount, aRetVal); entry->mValue.SetAsString() = value; } @@ -553,7 +548,7 @@ static void CollectFromSelectElement(JSContext* aCx, Document& aDocument, JS_ClearPendingException(aCx); continue; } - Record::EntryType* entry = + Record::EntryType* entry = AppendEntryToCollectedData(select, id, aGeneratedCount, aRetVal); entry->mValue.SetAsObject() = &jsval.toObject(); } else { @@ -565,8 +560,8 @@ static void CollectFromSelectElement(JSContext* aCx, Document& aDocument, } bool hasDefaultValue = true; nsTArray selectslist; - uint32_t numOptions = options->Length(); - for (uint32_t idx = 0; idx < numOptions; idx++) { + int numOptions = options->Length(); + for (int idx = 0; idx < numOptions; idx++) { HTMLOptionElement* option = options->ItemAsOption(idx); bool selected = option->Selected(); if (!selected) { @@ -586,7 +581,7 @@ static void CollectFromSelectElement(JSContext* aCx, Document& aDocument, JS_ClearPendingException(aCx); continue; } - Record::EntryType* entry = + Record::EntryType* entry = AppendEntryToCollectedData(select, id, aGeneratedCount, aRetVal); entry->mValue.SetAsObject() = &jsval.toObject(); } @@ -635,7 +630,7 @@ static void CollectFromXULTextbox(Document& aDocument, continue; } uint16_t generatedCount = 0; - Record::EntryType* entry = + Record::EntryType* entry = AppendEntryToCollectedData(input, id, generatedCount, aRetVal); entry->mValue.SetAsString() = value; return; @@ -673,474 +668,3 @@ static void CollectFromXULTextbox(Document& aDocument, uri->GetSpecIgnoringRef(aRetVal.mUrl.Construct()); } } - -MOZ_CAN_RUN_SCRIPT -static void SetElementAsString(Element* aElement, const nsAString& aValue) { - IgnoredErrorResult rv; - HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNodeOrNull(aElement); - if (textArea) { - textArea->SetValue(aValue, rv); - if (!rv.Failed()) { - nsContentUtils::DispatchInputEvent(aElement); - } - return; - } - HTMLInputElement* input = HTMLInputElement::FromNodeOrNull(aElement); - if (input) { - input->SetValue(aValue, CallerType::NonSystem, rv); - if (!rv.Failed()) { - nsContentUtils::DispatchInputEvent(aElement); - return; - } - } - input = HTMLInputElement::FromNodeOrNull(nsFocusManager::GetRedirectedFocus(aElement)); - if (input) { - input->SetValue(aValue, CallerType::NonSystem, rv); - if (!rv.Failed()) { - nsContentUtils::DispatchInputEvent(aElement); - } - } -} - -MOZ_CAN_RUN_SCRIPT -static void SetElementAsBool(Element* aElement, bool aValue) { - HTMLInputElement* input = HTMLInputElement::FromNodeOrNull(aElement); - if (input) { - bool checked = input->Checked(); - if (aValue != checked) { - input->SetChecked(aValue); - nsContentUtils::DispatchInputEvent(aElement); - } - } -} - -MOZ_CAN_RUN_SCRIPT -static void SetElementAsFiles(HTMLInputElement* aElement, - const CollectedFileListValue& aValue) { - nsTArray fileList; - IgnoredErrorResult rv; - aElement->MozSetFileNameArray(aValue.mFileList, rv); - if (rv.Failed()) { - return; - } - nsContentUtils::DispatchInputEvent(aElement); -} - -MOZ_CAN_RUN_SCRIPT -static void SetElementAsSelect(HTMLSelectElement* aElement, - const CollectedNonMultipleSelectValue& aValue) { - HTMLOptionsCollection* options = aElement->GetOptions(); - if (!options) { - return; - } - int32_t selectIdx = options->SelectedIndex(); - if (selectIdx >= 0) { - nsAutoString selectOptionVal; - options->ItemAsOption(selectIdx)->GetValue(selectOptionVal); - if (aValue.mValue.Equals(selectOptionVal)) { - return; - } - } - uint32_t numOptions = options->Length(); - for (uint32_t idx = 0; idx < numOptions; idx++) { - HTMLOptionElement* option = options->ItemAsOption(idx); - nsAutoString optionValue; - option->GetValue(optionValue); - if (aValue.mValue.Equals(optionValue)) { - aElement->SetSelectedIndex(idx); - nsContentUtils::DispatchInputEvent(aElement); - } - } -} - -MOZ_CAN_RUN_SCRIPT -static void SetElementAsMultiSelect(HTMLSelectElement* aElement, - const nsTArray& aValueArray) { - bool fireEvent = false; - HTMLOptionsCollection* options = aElement->GetOptions(); - if (!options) { - return; - } - uint32_t numOptions = options->Length(); - for (uint32_t idx = 0; idx < numOptions; idx++) { - HTMLOptionElement* option = options->ItemAsOption(idx); - nsAutoString optionValue; - option->GetValue(optionValue); - for (uint32_t i = 0, l = aValueArray.Length(); i < l; ++i) { - if (optionValue.Equals(aValueArray[i])) { - option->SetSelected(true); - if (!option->DefaultSelected()) { - fireEvent = true; - } - } - } - } - if (fireEvent) { - nsContentUtils::DispatchInputEvent(aElement); - } -} - -MOZ_CAN_RUN_SCRIPT -static void SetElementAsObject(JSContext* aCx, Element* aElement, - JS::Handle aObject) { - RefPtr input = HTMLInputElement::FromNodeOrNull(aElement); - if (input) { - if (input->ControlType() == NS_FORM_INPUT_FILE) { - CollectedFileListValue value; - if (value.Init(aCx, aObject)) { - SetElementAsFiles(input, value); - } else { - JS_ClearPendingException(aCx); - } - } - return; - } - RefPtr select = HTMLSelectElement::FromNodeOrNull(aElement); - if (select) { - // For Single Select Element - if (!select->Multiple()) { - CollectedNonMultipleSelectValue value; - if (value.Init(aCx, aObject)) { - SetElementAsSelect(select, value); - } else { - JS_ClearPendingException(aCx); - } - return; - } - - // For Multiple Selects Element - bool isArray = false; - JS_IsArrayObject(aCx, aObject, &isArray); - if (!isArray) { - return; - } - JS::Rooted arrayObj(aCx, &aObject.toObject()); - uint32_t arrayLength = 0; - if (!JS_GetArrayLength(aCx, arrayObj, &arrayLength)) { - JS_ClearPendingException(aCx); - return; - } - nsTArray array(arrayLength); - for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; arrayIdx++) { - JS::Rooted element(aCx); - if (!JS_GetElement(aCx, arrayObj, arrayIdx, &element)) { - JS_ClearPendingException(aCx); - return; - } - if (!element.isString()) { - return; - } - nsAutoJSString value; - if (!value.init(aCx, element)) { - JS_ClearPendingException(aCx); - return; - } - array.AppendElement(value); - } - SetElementAsMultiSelect(select, array); - } -} - -MOZ_CAN_RUN_SCRIPT -static void SetRestoreData(JSContext* aCx, Element* aElement, - JS::MutableHandle aObject) { - nsAutoString data; - if (nsContentUtils::StringifyJSON(aCx, aObject, data)) { - SetElementAsString(aElement, data); - } else { - JS_ClearPendingException(aCx); - } -} - -MOZ_CAN_RUN_SCRIPT -static void SetInnerHTML(Document& aDocument, const CollectedFormData& aData) { - RefPtr bodyElement = aDocument.GetBody(); - if (aDocument.HasFlag(NODE_IS_EDITABLE) && bodyElement) { - IgnoredErrorResult rv; - bodyElement->SetInnerHTML(aData.mInnerHTML.Value(), - aDocument.NodePrincipal(), rv); - if (!rv.Failed()) { - nsContentUtils::DispatchInputEvent(bodyElement); - } - } -} - -class FormDataParseContext : public txIParseContext { - public: - explicit FormDataParseContext(bool aCaseInsensitive) - : mIsCaseInsensitive(aCaseInsensitive) {} - - nsresult resolveNamespacePrefix(nsAtom* aPrefix, int32_t& aID) override { - if (aPrefix == nsGkAtoms::xul) { - aID = kNameSpaceID_XUL; - } else { - MOZ_ASSERT(nsDependentAtomString(aPrefix).EqualsLiteral("xhtml")); - aID = kNameSpaceID_XHTML; - } - return NS_OK; - } - - nsresult resolveFunctionCall(nsAtom* aName, int32_t aID, - FunctionCall** aFunction) override { - return NS_ERROR_XPATH_UNKNOWN_FUNCTION; - } - - bool caseInsensitiveNameTests() override { return mIsCaseInsensitive; } - - void SetErrorOffset(uint32_t aOffset) override {} - - private: - bool mIsCaseInsensitive; -}; - -static Element* FindNodeByXPath(JSContext* aCx, Document& aDocument, - const nsAString& aExpression) { - FormDataParseContext parsingContext(aDocument.IsHTMLDocument()); - IgnoredErrorResult rv; - nsAutoPtr expression( - aDocument.XPathEvaluator()->CreateExpression(aExpression, &parsingContext, - &aDocument, rv)); - if (rv.Failed()) { - return nullptr; - } - RefPtr result = expression->Evaluate( - aCx, aDocument, XPathResult::FIRST_ORDERED_NODE_TYPE, nullptr, rv); - if (rv.Failed()) { - return nullptr; - } - return Element::FromNodeOrNull(result->GetSingleNodeValue(rv)); -} - -MOZ_CAN_RUN_SCRIPT_BOUNDARY -/* static */ bool SessionStoreUtils::RestoreFormData( - const GlobalObject& aGlobal, Document& aDocument, - const CollectedFormData& aData) { - if (!aData.mUrl.WasPassed()) { - return true; - } - // Don't restore any data for the given frame if the URL - // stored in the form data doesn't match its current URL. - nsAutoCString url; - Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url); - if (!aData.mUrl.Value().Equals(url)) { - return false; - } - if (aData.mInnerHTML.WasPassed()) { - SetInnerHTML(aDocument, aData); - } - if (aData.mId.WasPassed()) { - for (auto& entry : aData.mId.Value().Entries()) { - RefPtr node = aDocument.GetElementById(entry.mKey); - if (entry.mValue.IsString()) { - SetElementAsString(node, entry.mValue.GetAsString()); - } else if (entry.mValue.IsBoolean()) { - SetElementAsBool(node, entry.mValue.GetAsBoolean()); - } else { - // For about:{sessionrestore,welcomeback} we saved the field as JSON to - // avoid nested instances causing humongous sessionstore.js files. - // cf. bug 467409 - JSContext* cx = aGlobal.Context(); - if (entry.mKey.EqualsLiteral("sessionData")) { - nsAutoCString url; - Unused << aDocument.GetDocumentURI()->GetSpecIgnoringRef(url); - if (url.EqualsLiteral("about:sessionrestore") || - url.EqualsLiteral("about:welcomeback")) { - JS::Rooted object( - cx, JS::ObjectValue(*entry.mValue.GetAsObject())); - SetRestoreData(cx, node, &object); - continue; - } - } - JS::Rooted object( - cx, JS::ObjectValue(*entry.mValue.GetAsObject())); - SetElementAsObject(cx, node, object); - } - } - } - if (aData.mXpath.WasPassed()) { - for (auto& entry : aData.mXpath.Value().Entries()) { - RefPtr node = FindNodeByXPath(aGlobal.Context(), aDocument, entry.mKey); - if (entry.mValue.IsString()) { - SetElementAsString(node, entry.mValue.GetAsString()); - } else if (entry.mValue.IsBoolean()) { - SetElementAsBool(node, entry.mValue.GetAsBoolean()); - } else { - JS::Rooted object( - aGlobal.Context(), JS::ObjectValue(*entry.mValue.GetAsObject())); - SetElementAsObject(aGlobal.Context(), node, object); - } - } - } - return true; -} - -/* Read entries in the session storage data contained in a tab's history. */ -static void ReadAllEntriesFromStorage( - nsPIDOMWindowOuter* aWindow, - nsTHashtable& aVisitedOrigins, - Record>& aRetVal) { - nsCOMPtr docShell = aWindow->GetDocShell(); - if (!docShell) { - return; - } - - Document* doc = aWindow->GetDoc(); - if (!doc) { - return; - } - nsCOMPtr principal = doc->NodePrincipal(); - if (!principal) { - return; - } - - nsAutoCString origin; - nsresult rv = principal->GetOrigin(origin); - if (NS_FAILED(rv) || aVisitedOrigins.Contains(origin)) { - // Don't read a host twice. - return; - } - - /* Completed checking for recursion and is about to read storage*/ - nsCOMPtr storageManager = do_QueryInterface(docShell); - if (!storageManager) { - return; - } - RefPtr storage; - storageManager->GetStorage(aWindow->GetCurrentInnerWindow(), principal, false, - getter_AddRefs(storage)); - if (!storage) { - return; - } - mozilla::IgnoredErrorResult result; - uint32_t len = storage->GetLength(*principal, result); - if (result.Failed() || len == 0) { - return; - } - int64_t storageUsage = storage->GetOriginQuotaUsage(); - if (storageUsage > StaticPrefs::browser_sessionstore_dom_storage_limit()) { - return; - } - - Record>::EntryType* recordEntry = nullptr; - for (uint32_t i = 0; i < len; i++) { - Record::EntryType entry; - mozilla::IgnoredErrorResult res; - storage->Key(i, entry.mKey, *principal, res); - if (res.Failed()) { - continue; - } - - storage->GetItem(entry.mKey, entry.mValue, *principal, res); - if (res.Failed()) { - continue; - } - - if (!recordEntry) { - recordEntry = aRetVal.Entries().AppendElement(); - recordEntry->mKey = NS_ConvertUTF8toUTF16(origin); - aVisitedOrigins.PutEntry(origin); - } - recordEntry->mValue.Entries().AppendElement(std::move(entry)); - } -} - -/* Collect Collect session storage from current frame and all child frame */ -static void CollectedSessionStorageInternal( - JSContext* aCx, BrowsingContext* aBrowsingContext, - nsTHashtable& aVisitedOrigins, - Record>& aRetVal) { - /* Collect session store from current frame */ - nsPIDOMWindowOuter* window = aBrowsingContext->GetDOMWindow(); - if (!window) { - return; - } - ReadAllEntriesFromStorage(window, aVisitedOrigins, aRetVal); - - /* Collect session storage from all child frame */ - nsCOMPtr docShell = window->GetDocShell(); - if (!docShell) { - return; - } - int32_t length; - nsresult rv = docShell->GetChildCount(&length); - if (NS_FAILED(rv)) { - return; - } - for (int32_t i = 0; i < length; ++i) { - nsCOMPtr item; - docShell->GetChildAt(i, getter_AddRefs(item)); - if (!item) { - return; - } - nsCOMPtr childDocShell(do_QueryInterface(item)); - if (!childDocShell) { - return; - } - bool isDynamic = false; - rv = childDocShell->GetCreatedDynamically(&isDynamic); - if (NS_SUCCEEDED(rv) && isDynamic) { - continue; - } - CollectedSessionStorageInternal( - aCx, nsDocShell::Cast(childDocShell)->GetBrowsingContext(), - aVisitedOrigins, aRetVal); - } -} - -/* static */ void SessionStoreUtils::CollectSessionStorage( - const GlobalObject& aGlobal, WindowProxyHolder& aWindow, - Record>& aRetVal) { - nsTHashtable visitedOrigins; - CollectedSessionStorageInternal(aGlobal.Context(), aWindow.get(), - visitedOrigins, aRetVal); -} - -/* static */ void SessionStoreUtils::RestoreSessionStorage( - const GlobalObject& aGlobal, nsIDocShell* aDocShell, - const Record>& aData) { - for (auto& entry : aData.Entries()) { - // NOTE: In capture() we record the full origin for the URI which the - // sessionStorage is being captured for. As of bug 1235657 this code - // stopped parsing any origins which have originattributes correctly, as - // it decided to use the origin attributes from the docshell, and try to - // interpret the origin as a URI. Since bug 1353844 this code now correctly - // parses the full origin, and then discards the origin attributes, to - // make the behavior line up with the original intentions in bug 1235657 - // while preserving the ability to read all session storage from - // previous versions. In the future, if this behavior is desired, we may - // want to use the spec instead of the origin as the key, and avoid - // transmitting origin attribute information which we then discard when - // restoring. - // - // If changing this logic, make sure to also change the principal - // computation logic in SessionStore::_sendRestoreHistory. - - // OriginAttributes are always after a '^' character - int32_t pos = entry.mKey.RFindChar('^'); - nsCOMPtr principal = BasePrincipal::CreateCodebasePrincipal( - NS_ConvertUTF16toUTF8(Substring(entry.mKey, 0, pos))); - nsresult rv; - nsCOMPtr storageManager = do_QueryInterface(aDocShell, &rv); - if (NS_FAILED(rv)) { - return; - } - RefPtr storage; - // 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. - // Null window because the current window doesn't match the principal yet - // and loads about:blank. - storageManager->CreateStorage(nullptr, principal, EmptyString(), false, getter_AddRefs(storage)); - if (!storage) { - continue; - } - for (auto& InnerEntry : entry.mValue.Entries()) { - IgnoredErrorResult result; - storage->SetItem(InnerEntry.mKey, InnerEntry.mValue, *principal, result); - if (result.Failed()) { - NS_WARNING("storage set item failed!"); - } - } - } -} diff --git a/toolkit/components/sessionstore/SessionStoreUtils.h b/toolkit/components/sessionstore/SessionStoreUtils.h index dff4ea3a9e8d..f68e286baecf 100644 --- a/toolkit/components/sessionstore/SessionStoreUtils.h +++ b/toolkit/components/sessionstore/SessionStoreUtils.h @@ -54,18 +54,6 @@ class SessionStoreUtils { static void CollectFormData(const GlobalObject& aGlobal, Document& aDocument, CollectedFormData& aRetVal); - - MOZ_CAN_RUN_SCRIPT_BOUNDARY - static bool RestoreFormData(const GlobalObject& aGlobal, Document& aDocument, - const CollectedFormData& aData); - - static void CollectSessionStorage( - const GlobalObject& aGlobal, WindowProxyHolder& aWindow, - Record>& aRetVal); - - static void RestoreSessionStorage( - const GlobalObject& aGlobal, nsIDocShell* aDocShell, - const Record>& aData); }; } // namespace dom diff --git a/toolkit/components/url-classifier/tests/mochitest/test_threathit_report.html b/toolkit/components/url-classifier/tests/mochitest/test_threathit_report.html index 7f760f769f12..828cb599fdd6 100644 --- a/toolkit/components/url-classifier/tests/mochitest/test_threathit_report.html +++ b/toolkit/components/url-classifier/tests/mochitest/test_threathit_report.html @@ -210,10 +210,7 @@ function testOnWindow(aTestData) { }); } SpecialPowers.pushPrefEnv( - {"set": [ - ["browser.safebrowsing.phishing.enabled", true], - ["dom.testing.sync-content-blocking-notifications", true], - ]}, + {"set": [["browser.safebrowsing.phishing.enabled", true]]}, test); function test() { diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index 8426ed760dc3..d887d94fda18 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -241,6 +241,7 @@ EXTRA_JS_MODULES += [ 'SelectParentHelper.jsm', 'ServiceRequest.jsm', 'Services.jsm', + 'sessionstore/FormData.jsm', 'ShortcutUtils.jsm', 'Sqlite.jsm', 'Timer.jsm', diff --git a/toolkit/modules/sessionstore/FormData.jsm b/toolkit/modules/sessionstore/FormData.jsm new file mode 100644 index 000000000000..7cae9d4cf230 --- /dev/null +++ b/toolkit/modules/sessionstore/FormData.jsm @@ -0,0 +1,280 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["FormData"]; + +/** + * Returns whether the given URL very likely has input + * fields that contain serialized session store data. + */ +function isRestorationPage(url) { + return url == "about:sessionrestore" || url == "about:welcomeback"; +} + +/** + * Returns whether the given form |data| object contains nested restoration + * data for a page like about:sessionrestore or about:welcomeback. + */ +function hasRestorationData(data) { + if (isRestorationPage(data.url) && data.id) { + return typeof(data.id.sessionData) == "object"; + } + + return false; +} + +/** + * Returns the given document's current URI and strips + * off the URI's anchor part, if any. + */ +function getDocumentURI(doc) { + return doc.documentURI.replace(/#.*$/, ""); +} + +/** + * The public API exported by this module that allows to collect + * and restore form data for a document and its subframes. + */ +var FormData = Object.freeze({ + restore(frame, data) { + return FormDataInternal.restore(frame, data); + }, + + restoreTree(root, data) { + FormDataInternal.restoreTree(root, data); + }, +}); + +/** + * This module's internal API. + */ +var FormDataInternal = { + namespaceURIs: { + "xhtml": "http://www.w3.org/1999/xhtml", + "xul": "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + }, + + /** + * Resolves an XPath query generated by node.generateXPath. + */ + resolve(aDocument, aQuery) { + let xptype = aDocument.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE; + return aDocument.evaluate(aQuery, aDocument, this.resolveNS.bind(this), xptype, null).singleNodeValue; + }, + + /** + * Namespace resolver for the above XPath resolver. + */ + resolveNS(aPrefix) { + return this.namespaceURIs[aPrefix] || null; + }, + + /** + * @returns an XPath query to all savable form field nodes + */ + get restorableFormNodesXPath() { + let formNodesXPath = "//textarea|//xhtml:textarea|" + + "//select|//xhtml:select|" + + "//input|//xhtml:input" + + // Special case for about:config's search field. + "|/xul:window[@id='config']//xul:textbox[@id='textbox']"; + + delete this.restorableFormNodesXPath; + return (this.restorableFormNodesXPath = formNodesXPath); + }, + + /** + * Restores form |data| for the given frame. The data is expected to be in + * the same format that FormData.collect() returns. + * + * @param frame (DOMWindow) + * The frame to restore form data to. + * @param data (object) + * An object holding form data. + */ + restore({document: doc}, data) { + if (!data.url) { + return true; + } + + // Don't restore any data for the given frame if the URL + // stored in the form data doesn't match its current URL. + if (data.url != getDocumentURI(doc)) { + return false; + } + + // For about:{sessionrestore,welcomeback} we saved the field as JSON to + // avoid nested instances causing humongous sessionstore.js files. + // cf. bug 467409 + if (hasRestorationData(data)) { + data.id.sessionData = JSON.stringify(data.id.sessionData); + } + + if ("id" in data) { + let retrieveNode = id => doc.getElementById(id); + this.restoreManyInputValues(data.id, retrieveNode); + } + + if ("xpath" in data) { + let retrieveNode = xpath => this.resolve(doc, xpath); + this.restoreManyInputValues(data.xpath, retrieveNode); + } + + if ("innerHTML" in data) { + if (doc.body && doc.designMode == "on") { + // eslint-disable-next-line no-unsanitized/property + doc.body.innerHTML = data.innerHTML; + this.fireInputEvent(doc.body); + } + } + + return true; + }, + + /** + * Iterates the given form data, retrieving nodes for all the keys and + * restores their appropriate values. + * + * @param data (object) + * A subset of the form data as collected by FormData.collect(). This + * is either data stored under "id" or under "xpath". + * @param retrieve (function) + * The function used to retrieve the input field belonging to a key + * in the given |data| object. + */ + restoreManyInputValues(data, retrieve) { + for (let key of Object.keys(data)) { + let input = retrieve(key); + if (input) { + this.restoreSingleInputValue(input, data[key]); + } + } + }, + + /** + * Restores a given form value to a given DOMNode and takes care of firing + * the appropriate DOM event should the input's value change. + * + * @param aNode + * DOMNode to set form value on. + * @param aValue + * Value to set form element to. + */ + restoreSingleInputValue(aNode, aValue) { + let fireEvent = false; + + 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; + fireEvent = true; + } else if (typeof aValue == "boolean") { + // Don't dispatch a change event for no change. + if (aNode.checked == aValue) { + return; + } + + aNode.checked = aValue; + fireEvent = true; + } else if (aValue && aValue.selectedIndex >= 0 && aValue.value) { + // 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; + fireEvent = true; + break; + } + } + } else if (aValue && aValue.fileList && aValue.type == "file" && + aNode.type == "file") { + try { + // FIXME (bug 1122855): This won't work in content processes. + aNode.mozSetFileNameArray(aValue.fileList, aValue.fileList.length); + } catch (e) { + Cu.reportError("mozSetFileNameArray: " + e); + } + fireEvent = true; + } else if (Array.isArray(aValue) && 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) { + fireEvent = true; + } + }); + } + + // Fire events for this node if applicable + if (fireEvent) { + this.fireInputEvent(aNode); + } + }, + + /** + * Dispatches an event of type "input" to the given |node|. + * + * @param node (DOMNode) + */ + fireInputEvent(node) { + // "inputType" value hasn't been decided for session restor: + // https://github.com/w3c/input-events/issues/30#issuecomment-438693664 + let event = node.isInputEventTarget ? + new node.ownerGlobal.InputEvent("input", {bubbles: true, inputType: ""}) : + new node.ownerGlobal.Event("input", {bubbles: true}); + node.dispatchEvent(event); + }, + + /** + * Restores form data for the current frame hierarchy starting at |root| + * using the given form |data|. + * + * If the given |root| frame's hierarchy doesn't match that of the given + * |data| object we will silently discard data for unreachable frames. For + * security reasons we will never restore form data to the wrong frames as + * we bail out silently if the stored URL doesn't match the frame's current + * URL. + * + * @param root (DOMWindow) + * @param data (object) + * { + * formdata: {id: {input1: "value1"}}, + * children: [ + * {formdata: {id: {input2: "value2"}}}, + * null, + * {formdata: {xpath: { ... }}, children: [ ... ]} + * ] + * } + */ + restoreTree(root, data) { + // Restore data for the given |root| frame and its descendants. If restore() + // returns false this indicates the |data.url| doesn't match the loaded + // document URI. We then must ignore this branch for security reasons. + if (this.restore(root, data) === false) { + return; + } + + if (!data.hasOwnProperty("children")) { + return; + } + + let frames = root.frames; + for (let index of Object.keys(data.children)) { + if (index < frames.length) { + this.restoreTree(frames[index], data.children[index]); + } + } + }, +};