diff --git a/browser/components/sessionstore/test/browser_capabilities.js b/browser/components/sessionstore/test/browser_capabilities.js index eae2567da56c..f02b0ede76bd 100644 --- a/browser/components/sessionstore/test/browser_capabilities.js +++ b/browser/components/sessionstore/test/browser_capabilities.js @@ -11,7 +11,7 @@ add_task(async function docshell_capabilities() { let tab = await createTab(); let browser = tab.linkedBrowser; - let docShell = browser.docShell; + let { browsingContext, docShell } = browser; // Get the list of capabilities for docShells. let flags = Object.keys(docShell).filter(k => k.startsWith("allow")); @@ -27,7 +27,7 @@ add_task(async function docshell_capabilities() { // Flip a couple of allow* flags. docShell.allowImages = false; docShell.allowMetaRedirects = false; - docShell.allowJavascript = false; + browsingContext.allowJavascript = false; // Now reload the document to ensure that these capabilities // are taken into account. @@ -68,7 +68,7 @@ add_task(async function docshell_capabilities() { ok(!docShell.allowMetaRedirects, "meta redirects not allowed"); // Check that docShell allowJavascript flag is not set. - ok(docShell.allowJavascript, "Javascript still allowed"); + ok(browsingContext.allowJavascript, "Javascript still allowed"); // Check that we correctly restored features as disabled. state = JSON.parse(ss.getTabState(tab)); diff --git a/caps/tests/mochitest/test_disableScript.xhtml b/caps/tests/mochitest/test_disableScript.xhtml index fe07f7fe6b07..ac514eb38740 100644 --- a/caps/tests/mochitest/test_disableScript.xhtml +++ b/caps/tests/mochitest/test_disableScript.xhtml @@ -85,8 +85,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=840488 is(win.wrappedJSObject.gFiredOnclick, expectEnabled, "Checking script-enabled for " + win.name + " (" + win.location + ")"); } - function setScriptEnabledForDocShell(win, enabled) { - win.docShell.allowJavascript = enabled; + function setScriptEnabled(win, enabled) { + win.browsingContext.allowJavascript = enabled; } function testList(expectEnabled, win, list, idx) { @@ -138,16 +138,16 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=840488 // Test simple docshell enable/disable. checkScriptEnabled(rootWin, true); - setScriptEnabledForDocShell(rootWin, false); + setScriptEnabled(rootWin, false); checkScriptEnabled(rootWin, false); - setScriptEnabledForDocShell(rootWin, true); + setScriptEnabled(rootWin, true); checkScriptEnabled(rootWin, true); // Privileged frames are immune to docshell flags. ok(chromeWin.document.nodePrincipal.isSystemPrincipal, "Sanity check for System Principal"); - setScriptEnabledForDocShell(chromeWin, false); + setScriptEnabled(chromeWin, false); checkScriptEnabled(chromeWin, true); - setScriptEnabledForDocShell(chromeWin, true); + setScriptEnabled(chromeWin, true); // Play around with the docshell tree and make sure everything works as // we expect. @@ -156,32 +156,32 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=840488 return addFrame(rootWin[0], 'childA', true); }).then(function() { checkScriptEnabled(rootWin[0][0], true); - setScriptEnabledForDocShell(rootWin[0], false); + setScriptEnabled(rootWin[0], false); checkScriptEnabled(rootWin, true); checkScriptEnabled(rootWin[0], false); checkScriptEnabled(rootWin[0][0], false); return addFrame(rootWin[0], 'childB', false); }).then(function() { checkScriptEnabled(rootWin[0][1], false); - setScriptEnabledForDocShell(rootWin[0][0], false); - setScriptEnabledForDocShell(rootWin[0], true); + setScriptEnabled(rootWin[0][0], false); + setScriptEnabled(rootWin[0], true); checkScriptEnabled(rootWin[0], true); checkScriptEnabled(rootWin[0][0], false); - setScriptEnabledForDocShell(rootWin[0][0], true); + setScriptEnabled(rootWin[0][0], true); // Flags are inherited from the parent docshell at attach time. Note that // the flag itself is inherited, regardless of whether or not scripts are // currently allowed on the parent (which could depend on the parent's // parent). Check that. checkScriptEnabled(rootWin[0][1], false); - setScriptEnabledForDocShell(rootWin[0], false); - setScriptEnabledForDocShell(rootWin[0][1], true); + setScriptEnabled(rootWin[0], false); + setScriptEnabled(rootWin[0][1], true); return addFrame(rootWin[0][1], 'grandchild', false); }).then(function() { checkScriptEnabled(rootWin[0], false); checkScriptEnabled(rootWin[0][1], false); checkScriptEnabled(rootWin[0][1][0], false); - setScriptEnabledForDocShell(rootWin[0], true); + setScriptEnabled(rootWin[0], true); checkScriptEnabled(rootWin[0], true); checkScriptEnabled(rootWin[0][1], true); checkScriptEnabled(rootWin[0][1][0], true); @@ -195,8 +195,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=840488 .then(function() { checkScriptEnabled(rootWin[0][0], true); checkScriptEnabled(rootWin[0][1][0], true); - setScriptEnabledForDocShell(rootWin[0][0], false); - setScriptEnabledForDocShell(rootWin[0][1], false); + setScriptEnabled(rootWin[0][0], false); + setScriptEnabled(rootWin[0][1], false); checkScriptEnabled(rootWin[0][0], false); checkScriptEnabled(rootWin[0][1][0], false); return navigateBack(rootWin[0][0].frameElement); diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js.js b/devtools/client/framework/test/browser_toolbox_options_disable_js.js index 3ee7b1903144..0018420e791e 100644 --- a/devtools/client/framework/test/browser_toolbox_options_disable_js.js +++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.js @@ -34,8 +34,8 @@ add_task(async function() { async function testJSEnabled() { info("Testing that JS is enabled"); - // We use waitForTick here because switching docShell.allowJavascript to true - // takes a while to become live. + // We use waitForTick here because switching browsingContext.allowJavascript + // to true takes a while to become live. await waitForTick(); await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() { diff --git a/devtools/client/framework/toolbox-options.js b/devtools/client/framework/toolbox-options.js index b12d50399ce2..6656614f2c97 100644 --- a/devtools/client/framework/toolbox-options.js +++ b/devtools/client/framework/toolbox-options.js @@ -605,10 +605,10 @@ OptionsPanel.prototype = { /** * Disables JavaScript for the currently loaded tab. We force a page refresh - * here because setting docShell.allowJavascript to true fails to block JS - * execution from event listeners added using addEventListener(), AJAX calls - * and timers. The page refresh prevents these things from being added in the - * first place. + * here because setting browsingContext.allowJavascript to true fails to block + * JS execution from event listeners added using addEventListener(), AJAX + * calls and timers. The page refresh prevents these things from being added + * in the first place. * * @param {Event} event * The event sent by checking / unchecking the disable JS checkbox. diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index 67850e445bd4..a20e71891e3d 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -2115,14 +2115,18 @@ Toolbox.prototype = { }, /** - * Read the initial javascriptEnabled configuration from the current target - * and forward it to the configuration actor. + * If we have an older version of the server which handles `javascriptEnabled` + * in the browsing-context target, read the initial javascriptEnabled + * configuration from the current target and forward it to the configuration + * actor. */ _applyJavascriptEnabledSettings: function() { - const javascriptEnabled = this.target._javascriptEnabled; - this.commands.targetConfigurationCommand.updateConfiguration({ - javascriptEnabled, - }); + if (this.target.traits.javascriptEnabled) { + const javascriptEnabled = this.target._javascriptEnabled; + this.commands.targetConfigurationCommand.updateConfiguration({ + javascriptEnabled, + }); + } }, /** diff --git a/devtools/client/fronts/targets/browsing-context.js b/devtools/client/fronts/targets/browsing-context.js index ecaad6e0dd8d..180585b3b1f3 100644 --- a/devtools/client/fronts/targets/browsing-context.js +++ b/devtools/client/fronts/targets/browsing-context.js @@ -20,9 +20,8 @@ class BrowsingContextTargetFront extends TargetMixin( // For targets which support the Watcher and configuration actor, the status // for the `javascriptEnabled` setting will be available on the configuration - // front, and the target will only be used to read the initial value. - // For other targets, _javascriptEnabled will be updated everytime - // `reconfigure` is called. + // front, and the target will only be used to read the initial value from older + // servers. // Note: this property is marked as private but is accessed by the // TargetCommand to provide the "isJavascriptEnabled" wrapper. It should NOT be // used anywhere else. @@ -121,9 +120,13 @@ class BrowsingContextTargetFront extends TargetMixin( const response = await super.attach(); this.targetForm.threadActor = response.threadActor; - this._javascriptEnabled = response.javascriptEnabled; this.traits = response.traits || {}; + if (response.javascriptEnabled != null) { + this.traits.javascriptEnabled = true; + this._javascriptEnabled = response.javascriptEnabled; + } + // xpcshell tests from devtools/server/tests/xpcshell/ are implementing // fake BrowsingContextTargetActor which do not expose any console actor. if (this.targetForm.consoleActor) { @@ -133,16 +136,6 @@ class BrowsingContextTargetFront extends TargetMixin( return this._attach; } - async reconfigure({ options }) { - const response = await super.reconfigure({ options }); - - if (typeof options.javascriptEnabled != "undefined") { - this._javascriptEnabled = options.javascriptEnabled; - } - - return response; - } - async detach() { // When calling this.destroy() at the end of this method, // we will end up calling detach again from TargetMixin.destroy. diff --git a/devtools/server/actors/target-configuration.js b/devtools/server/actors/target-configuration.js index d2c442ba6eb0..ab1225a8ef48 100644 --- a/devtools/server/actors/target-configuration.js +++ b/devtools/server/actors/target-configuration.js @@ -193,6 +193,17 @@ const TargetConfigurationActor = ActorClassWithSpec(targetConfigurationSpec, { case "customUserAgent": this._setCustomUserAgent(value); break; + case "javascriptEnabled": + if (value !== undefined) { + const reload = value != this.isJavascriptEnabled(); + this._setJavascriptEnabled(value); + // This flag requires a reload in order to take full effect, + // so reload if it has changed. + if (reload) { + this._browsingContext.reload(0); + } + } + break; case "overrideDPPX": this._setDPPXOverride(value); break; @@ -243,6 +254,10 @@ const TargetConfigurationActor = ActorClassWithSpec(targetConfigurationSpec, { this._setDPPXOverride(this._initialDPPXOverride); } + if (this._initialJavascriptEnabled !== undefined) { + this._setJavascriptEnabled(this._initialJavascriptEnabled); + } + if (this._initialTouchEventsOverride !== undefined) { this._setTouchEventsOverride(this._initialTouchEventsOverride); } @@ -297,6 +312,18 @@ const TargetConfigurationActor = ActorClassWithSpec(targetConfigurationSpec, { this._browsingContext.customUserAgent = userAgent; }, + isJavascriptEnabled() { + return this._browsingContext.allowJavascript; + }, + _setJavascriptEnabled(allow) { + if (this._initialJavascriptEnabled === undefined) { + this._initialJavascriptEnabled = this._browsingContext.allowJavascript; + } + if (allow !== undefined) { + this._browsingContext.allowJavascript = allow; + } + }, + /* DPPX override */ _setDPPXOverride(dppx) { if (this._browsingContext.overrideDPPX === dppx) { diff --git a/devtools/server/actors/targets/browsing-context.js b/devtools/server/actors/targets/browsing-context.js index 852266eab6cd..d20307668763 100644 --- a/devtools/server/actors/targets/browsing-context.js +++ b/devtools/server/actors/targets/browsing-context.js @@ -1090,7 +1090,6 @@ const browsingContextTargetPrototype = { return { threadActor: this.threadActor.actorID, cacheDisabled: this._getCacheDisabled(), - javascriptEnabled: this._getJavascriptEnabled(), traits: this.traits, }; }, @@ -1278,14 +1277,6 @@ const browsingContextTargetPrototype = { // propagated through the browsing context tree via the platform. return; } - - if ( - typeof options.javascriptEnabled !== "undefined" && - options.javascriptEnabled !== this._getJavascriptEnabled() - ) { - this._setJavascriptEnabled(options.javascriptEnabled); - reload = true; - } if ( typeof options.cacheDisabled !== "undefined" && options.cacheDisabled !== this._getCacheDisabled() @@ -1320,7 +1311,6 @@ const browsingContextTargetPrototype = { * state when closing the toolbox. */ _restoreTargetConfiguration() { - this._restoreJavascript(); this._setCacheDisabled(false); this._setPaintFlashingEnabled(false); @@ -1339,39 +1329,6 @@ const browsingContextTargetPrototype = { this.docShell.defaultLoadFlags = disabled ? disable : enable; }, - /** - * Disable or enable JS via docShell. - */ - _wasJavascriptEnabled: null, - _setJavascriptEnabled(allow) { - if (this._wasJavascriptEnabled === null) { - this._wasJavascriptEnabled = this.docShell.allowJavascript; - } - this.docShell.allowJavascript = allow; - }, - - /** - * Restore JS state, before the actor modified it. - */ - _restoreJavascript() { - if (this._wasJavascriptEnabled !== null) { - this._setJavascriptEnabled(this._wasJavascriptEnabled); - this._wasJavascriptEnabled = null; - } - }, - - /** - * Return JS allowed status. - */ - _getJavascriptEnabled() { - if (!this.docShell) { - // The browsing context is already closed. - return null; - } - - return this.docShell.allowJavascript; - }, - /** * Disable or enable the paint flashing on the target. */ diff --git a/devtools/shared/commands/target-configuration/target-configuration-command.js b/devtools/shared/commands/target-configuration/target-configuration-command.js index 97ce765cfe05..14292c0c26e4 100644 --- a/devtools/shared/commands/target-configuration/target-configuration-command.js +++ b/devtools/shared/commands/target-configuration/target-configuration-command.js @@ -61,20 +61,23 @@ class TargetConfigurationCommand { } async isJavascriptEnabled() { - if ( - this._hasTargetWatcherSupport() && - // `javascriptEnabled` is first read by the target and then forwarded by - // the toolbox to the TargetConfigurationCommand, so it might be undefined at this - // point. - typeof this.configuration.javascriptEnabled !== "undefined" - ) { - return this.configuration.javascriptEnabled; + if (this._hasTargetWatcherSupport()) { + const front = await this.getFront(); + return front.isJavascriptEnabled(); } // If the TargetConfigurationActor does not know the value yet, or if the target don't - // support the Watcher + configuration actor, fallback on the initial value cached by - // the target front. - return this._commands.targetCommand.targetFront._javascriptEnabled; + // support the Watcher + configuration actor, and we have an old version of the server + // which handles `javascriptEnabled` in the target, fallback on the initial value + // cached by the target front. + const { targetFront } = this._commands.targetCommand; + if (targetFront.traits.javascriptEnabled) { + return targetFront._javascriptEnabled; + } + + // If we don't have target watcher support, we can't get this value, so just fall back + // to the default. + return true; } /** diff --git a/devtools/shared/specs/target-configuration.js b/devtools/shared/specs/target-configuration.js index d812f52dcebc..ebf9d44f5ee6 100644 --- a/devtools/shared/specs/target-configuration.js +++ b/devtools/shared/specs/target-configuration.js @@ -38,6 +38,12 @@ const targetConfigurationSpec = generateActorSpec({ configuration: RetVal("target-configuration.configuration"), }, }, + isJavascriptEnabled: { + request: {}, + response: { + javascriptEnabled: RetVal("boolean"), + }, + }, }, }); diff --git a/devtools/shared/specs/targets/browsing-context.js b/devtools/shared/specs/targets/browsing-context.js index 1270d2657c32..a7ab6a57a9ad 100644 --- a/devtools/shared/specs/targets/browsing-context.js +++ b/devtools/shared/specs/targets/browsing-context.js @@ -14,7 +14,7 @@ const { types.addDictType("browsingContextTarget.attach", { threadActor: "number", cacheDisabled: "boolean", - javascriptEnabled: "boolean", + javascriptEnabled: "nullable:boolean", traits: "json", }); @@ -46,7 +46,6 @@ types.addDictType("browsingContextTarget.reload", { types.addDictType("browsingContextTarget.reconfigure", { cacheDisabled: "nullable:boolean", colorSchemeSimulation: "nullable:string", - javascriptEnabled: "nullable:boolean", paintFlashing: "nullable:boolean", printSimulationEnabled: "nullable:boolean", restoreFocus: "nullable:boolean", diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp index 93fbae328cfa..fac8b44b6ec0 100644 --- a/docshell/base/BrowsingContext.cpp +++ b/docshell/base/BrowsingContext.cpp @@ -406,6 +406,8 @@ already_AddRefed BrowsingContext::CreateDetached( fields.mTouchEventsOverrideInternal = TouchEventsOverride::None; + fields.mAllowJavascript = inherit ? inherit->GetAllowJavascript() : true; + RefPtr context; if (XRE_IsParentProcess()) { context = new CanonicalBrowsingContext(parentWC, group, id, @@ -538,6 +540,7 @@ BrowsingContext::BrowsingContext(WindowContext* aParentWindow, mUseRemoteSubframes(false), mCreatedDynamically(false), mIsInBFCache(false), + mCanExecuteScripts(true), mChildOffset(0) { MOZ_RELEASE_ASSERT(!mParentWindow || mParentWindow->Group() == mGroup); MOZ_RELEASE_ASSERT(mBrowsingContextId != 0); @@ -742,6 +745,7 @@ void BrowsingContext::Attach(bool aFromIPC, ContentParent* aOriginProcess) { mChildOffset = mCreatedDynamically ? -1 : mParentWindow->Children().Length(); mParentWindow->AppendChildBrowsingContext(this); + RecomputeCanExecuteScripts(); } else { mGroup->Toplevels().AppendElement(this); } @@ -2628,6 +2632,43 @@ void BrowsingContext::DidSet(FieldIndex, Group()->UpdateToplevelsSuspendedIfNeeded(); } +auto BrowsingContext::CanSet(FieldIndex, bool aValue, + ContentParent* aSource) -> CanSetResult { + if (mozilla::SessionHistoryInParent()) { + return XRE_IsParentProcess() && !aSource ? CanSetResult::Allow : CanSetResult::Deny; + } + + // Without Session History in Parent, session restore code still needs to set + // this from content processes. + return LegacyRevertIfNotOwningOrParentProcess(aSource); +} + +void BrowsingContext::DidSet(FieldIndex, bool aOldValue) { + RecomputeCanExecuteScripts(); +} + + +void BrowsingContext::RecomputeCanExecuteScripts() { + const bool old = mCanExecuteScripts; + if (!AllowJavascript()) { + // Scripting has been explicitly disabled on our BrowsingContext. + mCanExecuteScripts = false; + } else if (GetParentWindowContext()) { + // Otherwise, inherit parent. + mCanExecuteScripts = GetParentWindowContext()->CanExecuteScripts(); + } else { + // Otherwise, we're the root of the tree, and we haven't explicitly disabled + // script. Allow. + mCanExecuteScripts = true; + } + + if (old != mCanExecuteScripts) { + for (WindowContext* wc : GetWindowContexts()) { + wc->RecomputeCanExecuteScripts(); + } + } +} + bool BrowsingContext::InactiveForSuspend() const { if (!StaticPrefs::dom_suspend_inactive_enabled()) { return false; diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h index bc216d84783d..7a5be57ad3fb 100644 --- a/docshell/base/BrowsingContext.h +++ b/docshell/base/BrowsingContext.h @@ -200,7 +200,10 @@ enum class ExplicitActiveStatus : uint8_t { /* Don't use the getter of the field, but IsInBFCache() method */ \ FIELD(IsInBFCache, bool) \ FIELD(HasRestoreData, bool) \ - FIELD(SessionStoreEpoch, uint32_t) + FIELD(SessionStoreEpoch, uint32_t) \ + /* Whether we can execute scripts in this BrowsingContext. Has no effect \ + * unless scripts are also allowed in the parent WindowContext. */ \ + FIELD(AllowJavascript, bool) // BrowsingContext, in this context, is the cross process replicated // environment in which information about documents is stored. In @@ -851,6 +854,9 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { bool IsInBFCache() const { return mIsInBFCache; } + bool AllowJavascript() const { return GetAllowJavascript(); } + bool CanExecuteScripts() const { return mCanExecuteScripts; } + protected: virtual ~BrowsingContext(); BrowsingContext(WindowContext* aParentWindow, BrowsingContextGroup* aGroup, @@ -865,6 +871,12 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { private: void Attach(bool aFromIPC, ContentParent* aOriginProcess); + // Recomputes whether we can execute scripts in this BrowsingContext based on + // the value of AllowJavascript() and whether scripts are allowed in the + // parent WindowContext. Called whenever the AllowJavascript() flag or the + // parent WC changes. + void RecomputeCanExecuteScripts(); + // Find the special browsing context if aName is '_self', '_parent', // '_top', but not '_blank'. The latter is handled in FindWithName BrowsingContext* FindWithSpecialName(const nsAString& aName, @@ -1069,6 +1081,10 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { ContentParent* aSource); void DidSet(FieldIndex, bool aOldValue); + CanSetResult CanSet(FieldIndex, bool aValue, + ContentParent* aSource); + void DidSet(FieldIndex, bool aOldValue); + bool CanSet(FieldIndex, bool aNewValue, ContentParent* aSource); @@ -1185,6 +1201,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { // before dispatching pageshow. bool mIsInBFCache : 1; + // Determines if we can execute scripts in this BrowsingContext. True if + // AllowJavascript() is true and script execution is allowed in the parent + // WindowContext. + bool mCanExecuteScripts : 1; + // The original offset of this context in its container. This property is -1 // if this BrowsingContext is for a frame that was added dynamically. int32_t mChildOffset; diff --git a/docshell/base/WindowContext.cpp b/docshell/base/WindowContext.cpp index 3801d937fa0b..0e79477bf135 100644 --- a/docshell/base/WindowContext.cpp +++ b/docshell/base/WindowContext.cpp @@ -262,6 +262,44 @@ bool WindowContext::CanSet(FieldIndex, const bool& aValue, return IsTop(); } +bool WindowContext::CanSet(FieldIndex, bool aValue, + ContentParent* aSource) { + return (XRE_IsParentProcess() && !aSource) || CheckOnlyOwningProcessCanSet(aSource); +} + +void WindowContext::DidSet(FieldIndex, bool aOldValue) { + RecomputeCanExecuteScripts(); +} + +void WindowContext::RecomputeCanExecuteScripts(bool aApplyChanges) { + const bool old = mCanExecuteScripts; + if (!AllowJavascript()) { + // Scripting has been explicitly disabled on our WindowContext. + mCanExecuteScripts = false; + } else { + // Otherwise, inherit. + mCanExecuteScripts = mBrowsingContext->CanExecuteScripts(); + } + + if (aApplyChanges && old != mCanExecuteScripts) { + // Inform our active DOM window. + if (nsGlobalWindowInner* window = GetInnerWindow()) { + // Only update scriptability if the window is current. Windows will have + // scriptability disabled when entering the bfcache and updated when + // coming out. + if (window->IsCurrentInnerWindow()) { + auto& scriptability = xpc::Scriptability::Get( + window->GetGlobalJSObject()); + scriptability.SetWindowAllowsScript(mCanExecuteScripts); + } + } + + for (const RefPtr& child : Children()) { + child->RecomputeCanExecuteScripts(); + } + } +} + void WindowContext::DidSet(FieldIndex, bool aOldValue) { MOZ_ASSERT( @@ -473,6 +511,7 @@ WindowContext::WindowContext(BrowsingContext* aBrowsingContext, MOZ_ASSERT(mBrowsingContext); MOZ_ASSERT(mInnerWindowId); MOZ_ASSERT(mOuterWindowId); + RecomputeCanExecuteScripts(/* aApplyChanges */ false); } WindowContext::~WindowContext() { diff --git a/docshell/base/WindowContext.h b/docshell/base/WindowContext.h index 0da53cffd303..f69eaacb22da 100644 --- a/docshell/base/WindowContext.h +++ b/docshell/base/WindowContext.h @@ -31,62 +31,65 @@ class WindowGlobalInit; class BrowsingContext; class BrowsingContextGroup; -#define MOZ_EACH_WC_FIELD(FIELD) \ - /* Whether the SHEntry associated with the current top-level \ - * window has already seen user interaction. \ - * As such, this will be reset to false when a new SHEntry is \ - * created without changing the WC (e.g. when using pushState or \ - * sub-frame navigation) \ - * This flag is set for optimization purposes, to avoid \ - * having to get the top SHEntry and update it on every \ - * user interaction. \ - * This is only meaningful on the top-level WC. */ \ - FIELD(SHEntryHasUserInteraction, bool) \ - FIELD(CookieBehavior, Maybe) \ - FIELD(IsOnContentBlockingAllowList, bool) \ - /* Whether the given window hierarchy is third party. See \ - * ThirdPartyUtil::IsThirdPartyWindow for details */ \ - FIELD(IsThirdPartyWindow, bool) \ - /* Whether this window's channel has been marked as a third-party \ - * tracking resource */ \ - FIELD(IsThirdPartyTrackingResourceWindow, bool) \ - FIELD(IsSecureContext, bool) \ - FIELD(IsOriginalFrameSource, bool) \ - /* Mixed-Content: If the corresponding documentURI is https, \ - * then this flag is true. */ \ - FIELD(IsSecure, bool) \ - /* Whether the user has overriden the mixed content blocker to allow \ - * mixed content loads to happen */ \ - FIELD(AllowMixedContent, bool) \ - /* Whether this window has registered a "beforeunload" event \ - * handler */ \ - FIELD(HasBeforeUnload, bool) \ - /* Controls whether the WindowContext is currently considered to be \ - * activated by a gesture */ \ - FIELD(UserActivationState, UserActivation::State) \ - FIELD(EmbedderPolicy, nsILoadInfo::CrossOriginEmbedderPolicy) \ - /* True if this document tree contained at least a HTMLMediaElement. \ - * This should only be set on top level context. */ \ - FIELD(DocTreeHadMedia, bool) \ - FIELD(AutoplayPermission, uint32_t) \ - FIELD(ShortcutsPermission, uint32_t) \ - /* Store the Id of the browsing context where active media session \ - * exists on the top level window context */ \ - FIELD(ActiveMediaSessionContextId, Maybe) \ - /* ALLOW_ACTION if it is allowed to open popups for the sub-tree \ - * starting and including the current WindowContext */ \ - FIELD(PopupPermission, uint32_t) \ - FIELD(DelegatedPermissions, \ - PermissionDelegateHandler::DelegatedPermissionList) \ - FIELD(DelegatedExactHostMatchPermissions, \ - PermissionDelegateHandler::DelegatedPermissionList) \ - FIELD(HasReportedShadowDOMUsage, bool) \ - /* Whether the principal of this window is for a local \ - * IP address */ \ - FIELD(IsLocalIP, bool) \ - /* Whether the corresponding document has `loading='lazy'` \ - * images; It won't become false if the image becomes non-lazy */ \ - FIELD(HadLazyLoadImage, bool) +#define MOZ_EACH_WC_FIELD(FIELD) \ + /* Whether the SHEntry associated with the current top-level \ + * window has already seen user interaction. \ + * As such, this will be reset to false when a new SHEntry is \ + * created without changing the WC (e.g. when using pushState or \ + * sub-frame navigation) \ + * This flag is set for optimization purposes, to avoid \ + * having to get the top SHEntry and update it on every \ + * user interaction. \ + * This is only meaningful on the top-level WC. */ \ + FIELD(SHEntryHasUserInteraction, bool) \ + FIELD(CookieBehavior, Maybe) \ + FIELD(IsOnContentBlockingAllowList, bool) \ + /* Whether the given window hierarchy is third party. See \ + * ThirdPartyUtil::IsThirdPartyWindow for details */ \ + FIELD(IsThirdPartyWindow, bool) \ + /* Whether this window's channel has been marked as a third-party \ + * tracking resource */ \ + FIELD(IsThirdPartyTrackingResourceWindow, bool) \ + FIELD(IsSecureContext, bool) \ + FIELD(IsOriginalFrameSource, bool) \ + /* Mixed-Content: If the corresponding documentURI is https, \ + * then this flag is true. */ \ + FIELD(IsSecure, bool) \ + /* Whether the user has overriden the mixed content blocker to allow \ + * mixed content loads to happen */ \ + FIELD(AllowMixedContent, bool) \ + /* Whether this window has registered a "beforeunload" event \ + * handler */ \ + FIELD(HasBeforeUnload, bool) \ + /* Controls whether the WindowContext is currently considered to be \ + * activated by a gesture */ \ + FIELD(UserActivationState, UserActivation::State) \ + FIELD(EmbedderPolicy, nsILoadInfo::CrossOriginEmbedderPolicy) \ + /* True if this document tree contained at least a HTMLMediaElement. \ + * This should only be set on top level context. */ \ + FIELD(DocTreeHadMedia, bool) \ + FIELD(AutoplayPermission, uint32_t) \ + FIELD(ShortcutsPermission, uint32_t) \ + /* Store the Id of the browsing context where active media session \ + * exists on the top level window context */ \ + FIELD(ActiveMediaSessionContextId, Maybe) \ + /* ALLOW_ACTION if it is allowed to open popups for the sub-tree \ + * starting and including the current WindowContext */ \ + FIELD(PopupPermission, uint32_t) \ + FIELD(DelegatedPermissions, \ + PermissionDelegateHandler::DelegatedPermissionList) \ + FIELD(DelegatedExactHostMatchPermissions, \ + PermissionDelegateHandler::DelegatedPermissionList) \ + FIELD(HasReportedShadowDOMUsage, bool) \ + /* Whether the principal of this window is for a local \ + * IP address */ \ + FIELD(IsLocalIP, bool) \ + /* Whether the corresponding document has `loading='lazy'` \ + * images; It won't become false if the image becomes non-lazy */ \ + FIELD(HadLazyLoadImage, bool) \ + /* Whether we can execute scripts in this WindowContext. Has no effect \ + * unless scripts are also allowed in the BrowsingContext. */ \ + FIELD(AllowJavascript, bool) class WindowContext : public nsISupports, public nsWrapperCache { MOZ_DECL_SYNCED_CONTEXT(WindowContext, MOZ_EACH_WC_FIELD) @@ -182,6 +185,9 @@ class WindowContext : public nsISupports, public nsWrapperCache { bool HadLazyLoadImage() const { return GetHadLazyLoadImage(); } + bool AllowJavascript() const { return GetAllowJavascript(); } + bool CanExecuteScripts() const { return mCanExecuteScripts; } + protected: WindowContext(BrowsingContext* aBrowsingContext, uint64_t aInnerWindowId, uint64_t aOuterWindowId, FieldValues&& aFields); @@ -271,6 +277,10 @@ class WindowContext : public nsISupports, public nsWrapperCache { bool CanSet(FieldIndex, const bool& aValue, ContentParent* aSource); + bool CanSet(FieldIndex, bool aValue, + ContentParent* aSource); + void DidSet(FieldIndex, bool aOldValue); + void DidSet(FieldIndex, bool aOldValue); void DidSet(FieldIndex, bool aOldValue); @@ -284,6 +294,11 @@ class WindowContext : public nsISupports, public nsWrapperCache { void DidSet(FieldIndex, T&& aOldValue) {} void DidSet(FieldIndex); + // Recomputes whether we can execute scripts in this WindowContext based on + // the value of AllowJavascript() and whether scripts are allowed in the + // BrowsingContext. + void RecomputeCanExecuteScripts(bool aApplyChanges = true); + const uint64_t mInnerWindowId; const uint64_t mOuterWindowId; RefPtr mBrowsingContext; @@ -298,6 +313,11 @@ class WindowContext : public nsISupports, public nsWrapperCache { bool mIsDiscarded = false; bool mIsInProcess = false; + // Determines if we can execute scripts in this WindowContext. True if + // AllowJavascript() is true and script execution is allowed in the + // BrowsingContext. + bool mCanExecuteScripts = true; + // The start time of user gesture, this is only available if the window // context is in process. TimeStamp mUserGestureStart; diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index a59edc6017d7..fb0327ee068b 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -393,7 +393,6 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, #endif mInitialized(false), mAllowSubframes(true), - mAllowJavascript(true), mAllowMetaRedirects(true), mAllowImages(true), mAllowMedia(true), @@ -407,7 +406,6 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, mDeviceSizeIsPageSize(false), mWindowDraggingAllowed(false), mInFrameSwap(false), - mCanExecuteScripts(false), mFiredUnloadEvent(false), mEODForCurrentDocument(false), mURIResultedInDocument(false), @@ -1749,14 +1747,6 @@ nsDocShell::SetAllowPlugins(bool aAllowPlugins) { return mBrowsingContext->SetAllowPlugins(aAllowPlugins); } -NS_IMETHODIMP -nsDocShell::GetAllowJavascript(bool* aAllowJavascript) { - NS_ENSURE_ARG_POINTER(aAllowJavascript); - - *aAllowJavascript = mAllowJavascript; - return NS_OK; -} - NS_IMETHODIMP nsDocShell::GetCssErrorReportingEnabled(bool* aEnabled) { MOZ_ASSERT(aEnabled); @@ -1770,13 +1760,6 @@ nsDocShell::SetCssErrorReportingEnabled(bool aEnabled) { return NS_OK; } -NS_IMETHODIMP -nsDocShell::SetAllowJavascript(bool aAllowJavascript) { - mAllowJavascript = aAllowJavascript; - RecomputeCanExecuteScripts(); - return NS_OK; -} - NS_IMETHODIMP nsDocShell::GetUsePrivateBrowsing(bool* aUsePrivateBrowsing) { NS_ENSURE_ARG_POINTER(aUsePrivateBrowsing); @@ -2653,50 +2636,6 @@ Maybe nsDocShell::GetInitialClientInfo() const { return innerWindow->GetClientInfo(); } -void nsDocShell::RecomputeCanExecuteScripts() { - bool old = mCanExecuteScripts; - RefPtr parent = GetInProcessParentDocshell(); - - // If we have no tree owner, that means that we've been detached from the - // docshell tree (this is distinct from having no parent docshell, which - // is the case for root docshells). It would be nice to simply disallow - // script in detached docshells, but bug 986542 demonstrates that this - // behavior breaks at least one website. - // - // So instead, we use our previous value, unless mAllowJavascript has been - // explicitly set to false. - if (!mTreeOwner) { - mCanExecuteScripts = mCanExecuteScripts && mAllowJavascript; - // If scripting has been explicitly disabled on our docshell, we're done. - } else if (!mAllowJavascript) { - mCanExecuteScripts = false; - // If we have a parent, inherit. - } else if (parent) { - mCanExecuteScripts = parent->mCanExecuteScripts; - // Otherwise, we're the root of the tree, and we haven't explicitly disabled - // script. Allow. - } else { - mCanExecuteScripts = true; - } - - // Inform our active DOM window. - // - // This will pass the outer, which will be in the scope of the active inner. - if (mScriptGlobal && mScriptGlobal->GetGlobalJSObject()) { - xpc::Scriptability& scriptability = - xpc::Scriptability::Get(mScriptGlobal->GetGlobalJSObject()); - scriptability.SetDocShellAllowsScript(mCanExecuteScripts); - } - - // If our value has changed, our children might be affected. Recompute their - // value as well. - if (old != mCanExecuteScripts) { - for (auto* child : mChildList.ForwardRange()) { - static_cast(child)->RecomputeCanExecuteScripts(); - } - } -} - nsresult nsDocShell::SetDocLoaderParent(nsDocLoader* aParent) { bool wasFrame = IsFrame(); @@ -2717,10 +2656,6 @@ nsresult nsDocShell::SetDocLoaderParent(nsDocLoader* aParent) { nsCOMPtr parentAsDocShell(do_QueryInterface(parent)); if (parentAsDocShell) { - if (mAllowJavascript && - NS_SUCCEEDED(parentAsDocShell->GetAllowJavascript(&value))) { - SetAllowJavascript(value); - } if (mAllowMetaRedirects && NS_SUCCEEDED(parentAsDocShell->GetAllowMetaRedirects(&value))) { SetAllowMetaRedirects(value); @@ -2755,9 +2690,6 @@ nsresult nsDocShell::SetDocLoaderParent(nsDocLoader* aParent) { mContentListener->SetParentContentListener(parentURIListener); } - // Our parent has changed. Recompute scriptability. - RecomputeCanExecuteScripts(); - // Inform windows when they're being removed from their parent. if (!aParent) { MaybeClearStorageAccessFlag(); @@ -2982,16 +2914,6 @@ nsDocShell::SetTreeOwner(nsIDocShellTreeOwner* aTreeOwner) { } } - // Our tree owner has changed. Recompute scriptability. - // - // Note that this is near-redundant with the recomputation in - // SetDocLoaderParent(), but not so for the root DocShell, where the call to - // SetTreeOwner() happens after the initial AddDocLoaderAsChildOfRoot(), - // and we never set another parent. Given that this is neither expensive nor - // performance-critical, let's be safe and unconditionally recompute this - // state whenever dependent state changes. - RecomputeCanExecuteScripts(); - return NS_OK; } @@ -7658,9 +7580,6 @@ nsresult nsDocShell::RestoreFromHistory() { // Make sure to not clobber the state of the child. Since AddChild // always clobbers it, save it off first. - bool allowJavascript; - childShell->GetAllowJavascript(&allowJavascript); - bool allowRedirects; childShell->GetAllowMetaRedirects(&allowRedirects); @@ -7684,7 +7603,6 @@ nsresult nsDocShell::RestoreFromHistory() { // child inherits our mPrivateBrowsingId, which is what we want. AddChild(childItem); - childShell->SetAllowJavascript(allowJavascript); childShell->SetAllowMetaRedirects(allowRedirects); childShell->SetAllowSubframes(allowSubframes); childShell->SetAllowImages(allowImages); @@ -13091,12 +13009,6 @@ NS_IMETHODIMP nsDocShell::ExitPrintPreview() { #endif } -NS_IMETHODIMP -nsDocShell::GetCanExecuteScripts(bool* aResult) { - *aResult = mCanExecuteScripts; - return NS_OK; -} - /* [infallible] */ NS_IMETHODIMP nsDocShell::GetIsTopLevelContentDocShell( bool* aIsTopLevelContentDocShell) { diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h index 59fa5c032616..6363f96bb671 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -952,7 +952,6 @@ class nsDocShell final : public nsDocLoader, void SetupReferrerInfoFromChannel(nsIChannel* aChannel); void SetReferrerInfo(nsIReferrerInfo* aReferrerInfo); void ReattachEditorToWindow(nsISHEntry* aSHEntry); - void RecomputeCanExecuteScripts(); void ClearFrameHistory(nsISHEntry* aEntry); // Determine if this type of load should update history. static bool ShouldUpdateGlobalHistory(uint32_t aLoadType); @@ -1245,7 +1244,6 @@ class nsDocShell final : public nsDocLoader, bool mInitialized : 1; bool mAllowSubframes : 1; - bool mAllowJavascript : 1; bool mAllowMetaRedirects : 1; bool mAllowImages : 1; bool mAllowMedia : 1; @@ -1260,11 +1258,6 @@ class nsDocShell final : public nsDocLoader, bool mWindowDraggingAllowed : 1; bool mInFrameSwap : 1; - // Because scriptability depends on the mAllowJavascript values of our - // ancestors, we cache the effective scriptability and recompute it when - // it might have changed; - bool mCanExecuteScripts : 1; - // This boolean is set to true right before we fire pagehide and generally // unset when we embed a new content viewer. While it's true no navigation // is allowed in this docshell. diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl index dcf0b8c00d70..e513294932c2 100644 --- a/docshell/base/nsIDocShell.idl +++ b/docshell/base/nsIDocShell.idl @@ -192,11 +192,6 @@ interface nsIDocShell : nsIDocShellTreeItem */ attribute boolean allowPlugins; - /** - * Whether to allow Javascript execution - */ - attribute boolean allowJavascript; - /** * Attribute stating if refresh based redirects can be allowed */ @@ -468,13 +463,6 @@ interface nsIDocShell : nsIDocShellTreeItem */ void exitPrintPreview(); - /** - * Whether this docshell can execute scripts based on its hierarchy. - * The rule of thumb here is that we disable js if this docshell or any - * of its parents disallow scripting. - */ - [infallible] readonly attribute boolean canExecuteScripts; - /** * The ID of the docshell in the session history. */ diff --git a/docshell/test/unit/AllowJavascriptChild.jsm b/docshell/test/unit/AllowJavascriptChild.jsm new file mode 100644 index 000000000000..41f50d42078f --- /dev/null +++ b/docshell/test/unit/AllowJavascriptChild.jsm @@ -0,0 +1,44 @@ +"use strict"; +var EXPORTED_SYMBOLS = ["AllowJavascriptChild"]; + +class AllowJavascriptChild extends JSWindowActorChild { + async receiveMessage(msg) { + switch (msg.name) { + case "CheckScriptsAllowed": + return this.checkScriptsAllowed(); + case "CheckFiredLoadEvent": + return this.contentWindow.wrappedJSObject.gFiredOnload; + case "CreateIframe": + return this.createIframe(msg.data.url); + } + return null; + } + + handleEvent(event) { + if (event.type === "load") { + this.sendAsyncMessage("LoadFired"); + } + } + + checkScriptsAllowed() { + let win = this.contentWindow; + + win.wrappedJSObject.gFiredOnclick = false; + win.document.body.click(); + return win.wrappedJSObject.gFiredOnclick; + } + + async createIframe(url) { + let doc = this.contentWindow.document; + + let iframe = doc.createElement("iframe"); + iframe.src = url; + doc.body.appendChild(iframe); + + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + + return iframe.browsingContext; + } +} diff --git a/docshell/test/unit/AllowJavascriptParent.jsm b/docshell/test/unit/AllowJavascriptParent.jsm new file mode 100644 index 000000000000..b7f71bab3716 --- /dev/null +++ b/docshell/test/unit/AllowJavascriptParent.jsm @@ -0,0 +1,31 @@ +"use strict"; +var EXPORTED_SYMBOLS = ["AllowJavascriptParent"]; + +let loadPromises = new WeakMap(); + +class AllowJavascriptParent extends JSWindowActorParent { + async receiveMessage(msg) { + switch (msg.name) { + case "LoadFired": + let bc = this.browsingContext; + let deferred = loadPromises.get(bc); + if (deferred) { + loadPromises.delete(bc); + deferred.resolve(this); + } + break; + } + } + + static promiseLoad(bc) { + let deferred = loadPromises.get(bc); + if (!deferred) { + deferred = {}; + deferred.promise = new Promise(resolve => { + deferred.resolve = resolve; + }); + loadPromises.set(bc, deferred); + } + return deferred.promise; + } +} diff --git a/docshell/test/unit/test_allowJavascript.js b/docshell/test/unit/test_allowJavascript.js new file mode 100644 index 000000000000..1c1623c0ff7c --- /dev/null +++ b/docshell/test/unit/test_allowJavascript.js @@ -0,0 +1,269 @@ +"use strict"; + +const { XPCShellContentUtils } = ChromeUtils.import( + "resource://testing-common/XPCShellContentUtils.jsm" +); + +XPCShellContentUtils.init(this); + +const ACTOR = "AllowJavascript"; + +const HTML = String.raw` + + + + + + + +`; + +const server = XPCShellContentUtils.createHttpServer({ + hosts: ["example.com", "example.org"], +}); + +server.registerPathHandler("/", (request, response) => { + response.setHeader("Content-Type", "text/html"); + response.write(HTML); +}); + +function getResourceURI(file) { + return Services.io.newFileURI(do_get_file(file)).spec; +} + +const { AllowJavascriptParent } = ChromeUtils.import( + getResourceURI("AllowJavascriptParent.jsm") +); + +async function assertScriptsAllowed(bc, expectAllowed, desc) { + let actor = bc.currentWindowGlobal.getActor(ACTOR); + let allowed = await actor.sendQuery("CheckScriptsAllowed"); + equal( + allowed, + expectAllowed, + `Scripts should be ${expectAllowed ? "" : "dis"}allowed for ${desc}` + ); +} + +async function assertLoadFired(bc, expectFired, desc) { + let actor = bc.currentWindowGlobal.getActor(ACTOR); + let fired = await actor.sendQuery("CheckFiredLoadEvent"); + equal( + fired, + expectFired, + `Should ${expectFired ? "" : "not "}have fired load for ${desc}` + ); +} + +function createSubframe(bc, url) { + let actor = bc.currentWindowGlobal.getActor(ACTOR); + return actor.sendQuery("CreateIframe", { url }); +} + +add_task(async function() { + ChromeUtils.registerWindowActor(ACTOR, { + allFrames: true, + child: { + moduleURI: getResourceURI("AllowJavascriptChild.jsm"), + events: { load: { capture: true } }, + }, + parent: { + moduleURI: getResourceURI("AllowJavascriptParent.jsm"), + }, + }); + + let page = await XPCShellContentUtils.loadContentPage("http://example.com/", { + remote: true, + remoteSubframes: true, + }); + + let bc = page.browsingContext; + + { + let oopFrame1 = await createSubframe(bc, "http://example.org/"); + let inprocFrame1 = await createSubframe(bc, "http://example.com/"); + + let oopFrame1OopSub = await createSubframe( + oopFrame1, + "http://example.com/" + ); + let inprocFrame1OopSub = await createSubframe( + inprocFrame1, + "http://example.org/" + ); + + equal( + oopFrame1.allowJavascript, + true, + "OOP BC should inherit allowJavascript from parent" + ); + equal( + inprocFrame1.allowJavascript, + true, + "In-process BC should inherit allowJavascript from parent" + ); + equal( + oopFrame1OopSub.allowJavascript, + true, + "OOP BC child should inherit allowJavascript from parent" + ); + equal( + inprocFrame1OopSub.allowJavascript, + true, + "In-process child BC should inherit allowJavascript from parent" + ); + + await assertLoadFired(bc, true, "top BC"); + await assertScriptsAllowed(bc, true, "top BC"); + + await assertLoadFired(oopFrame1, true, "OOP frame 1"); + await assertScriptsAllowed(oopFrame1, true, "OOP frame 1"); + + await assertLoadFired(inprocFrame1, true, "In-process frame 1"); + await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1"); + + await assertLoadFired(oopFrame1OopSub, true, "OOP frame 1 subframe"); + await assertScriptsAllowed(oopFrame1OopSub, true, "OOP frame 1 subframe"); + + await assertLoadFired( + inprocFrame1OopSub, + true, + "In-process frame 1 subframe" + ); + await assertScriptsAllowed( + inprocFrame1OopSub, + true, + "In-process frame 1 subframe" + ); + + bc.allowJavascript = false; + await assertScriptsAllowed(bc, false, "top BC with scripts disallowed"); + await assertScriptsAllowed( + oopFrame1, + false, + "OOP frame 1 with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame1, + false, + "In-process frame 1 with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + oopFrame1OopSub, + false, + "OOP frame 1 subframe with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame1OopSub, + false, + "In-process frame 1 subframe with top BC with scripts disallowed" + ); + + let oopFrame2 = await createSubframe(bc, "http://example.org/"); + let inprocFrame2 = await createSubframe(bc, "http://example.com/"); + + equal( + oopFrame2.allowJavascript, + false, + "OOP BC 2 should inherit allowJavascript from parent" + ); + equal( + inprocFrame2.allowJavascript, + false, + "In-process BC 2 should inherit allowJavascript from parent" + ); + + await assertLoadFired( + oopFrame2, + undefined, + "OOP frame 2 with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + oopFrame2, + false, + "OOP frame 2 with top BC with scripts disallowed" + ); + await assertLoadFired( + inprocFrame2, + undefined, + "In-process frame 2 with top BC with scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame2, + false, + "In-process frame 2 with top BC with scripts disallowed" + ); + + bc.allowJavascript = true; + await assertScriptsAllowed(bc, true, "top BC"); + + await assertScriptsAllowed(oopFrame1, true, "OOP frame 1"); + await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1"); + await assertScriptsAllowed(oopFrame1OopSub, true, "OOP frame 1 subframe"); + await assertScriptsAllowed( + inprocFrame1OopSub, + true, + "In-process frame 1 subframe" + ); + + await assertScriptsAllowed(oopFrame2, false, "OOP frame 2"); + await assertScriptsAllowed(inprocFrame2, false, "In-process frame 2"); + + oopFrame1.currentWindowGlobal.allowJavascript = false; + inprocFrame1.currentWindowGlobal.allowJavascript = false; + + await assertScriptsAllowed( + oopFrame1, + false, + "OOP frame 1 with second level WC scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame1, + false, + "In-process frame 1 with second level WC scripts disallowed" + ); + await assertScriptsAllowed( + oopFrame1OopSub, + false, + "OOP frame 1 subframe second level WC scripts disallowed" + ); + await assertScriptsAllowed( + inprocFrame1OopSub, + false, + "In-process frame 1 subframe with second level WC scripts disallowed" + ); + + oopFrame1.reload(0); + inprocFrame1.reload(0); + await AllowJavascriptParent.promiseLoad(oopFrame1); + await AllowJavascriptParent.promiseLoad(inprocFrame1); + + equal( + oopFrame1.currentWindowGlobal.allowJavascript, + true, + "WindowContext.allowJavascript does not persist after navigation for OOP frame 1" + ); + equal( + inprocFrame1.currentWindowGlobal.allowJavascript, + true, + "WindowContext.allowJavascript does not persist after navigation for in-process frame 1" + ); + + await assertScriptsAllowed(oopFrame1, true, "OOP frame 1"); + await assertScriptsAllowed(inprocFrame1, true, "In-process frame 1"); + } + + bc.allowJavascript = false; + + bc.reload(0); + await AllowJavascriptParent.promiseLoad(bc); + + await assertLoadFired(bc, undefined, "top BC with scripts disabled"); + await assertScriptsAllowed(bc, false, "top BC with scripts disabled"); + + await page.close(); +}); diff --git a/docshell/test/unit/xpcshell.ini b/docshell/test/unit/xpcshell.ini index 753114481070..f7cf598c548c 100644 --- a/docshell/test/unit/xpcshell.ini +++ b/docshell/test/unit/xpcshell.ini @@ -1,6 +1,11 @@ [DEFAULT] head = head_docshell.js +[test_allowJavascript.js] +skip-if = os == 'android' +support-files = + AllowJavascriptChild.jsm + AllowJavascriptParent.jsm [test_bug442584.js] [test_browsing_context_structured_clone.js] [test_URIFixup.js] diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index ebef9901f678..72b35eb964f4 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -1847,7 +1847,7 @@ WindowStateHolder::WindowStateHolder(nsGlobalWindowInner* aWindow) aWindow->Suspend(); // When a global goes into the bfcache, we disable script. - xpc::Scriptability::Get(mInnerWindowReflector).SetDocShellAllowsScript(false); + xpc::Scriptability::Get(mInnerWindowReflector).SetWindowAllowsScript(false); } WindowStateHolder::~WindowStateHolder() { @@ -2331,10 +2331,11 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, mBrowsingContext->SetWindowProxy(outer); } - // Set scriptability based on the state of the docshell. - bool allow = GetDocShell()->GetCanExecuteScripts(); + // Set scriptability based on the state of the WindowContext. + WindowContext* wc = mInnerWindow->GetWindowContext(); + bool allow = wc ? wc->CanExecuteScripts() : mBrowsingContext->CanExecuteScripts(); xpc::Scriptability::Get(GetWrapperPreserveColor()) - .SetDocShellAllowsScript(allow); + .SetWindowAllowsScript(allow); if (!aState) { // Get the "window" property once so it will be cached on our inner. We diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl index 1bdb405dffad..d2f226388764 100644 --- a/dom/chrome-webidl/BrowsingContext.webidl +++ b/dom/chrome-webidl/BrowsingContext.webidl @@ -193,6 +193,16 @@ interface BrowsingContext { */ readonly attribute TouchEventsOverride touchEventsOverride; + /** + * Partially determines whether script execution is allowed in this + * BrowsingContext. Script execution will be permitted only if this + * attribute is true and script execution is allowed in the parent + * WindowContext. + * + * May only be set in the parent process. + */ + [SetterThrows] attribute boolean allowJavascript; + /** * The nsID of the browsing context in the session history. */ diff --git a/dom/chrome-webidl/WindowGlobalActors.webidl b/dom/chrome-webidl/WindowGlobalActors.webidl index 7a89a88266d9..bfb70a6b7803 100644 --- a/dom/chrome-webidl/WindowGlobalActors.webidl +++ b/dom/chrome-webidl/WindowGlobalActors.webidl @@ -29,6 +29,16 @@ interface WindowContext { // True if the corresponding document has `loading='lazy'` images; // It won't become false if the image becomes non-lazy. readonly attribute boolean hadLazyLoadImage; + + /** + * Partially determines whether script execution is allowed in this + * BrowsingContext. Script execution will be permitted only if this + * attribute is true and script execution is allowed in the owner + * BrowsingContext. + * + * May only be set in the context's owning process. + */ + [SetterThrows] attribute boolean allowJavascript; }; // Keep this in sync with nsIContentViewer::PermitUnloadAction. diff --git a/dom/ipc/WindowGlobalActor.cpp b/dom/ipc/WindowGlobalActor.cpp index 5946de1d8ab4..8a3b49edd4d7 100644 --- a/dom/ipc/WindowGlobalActor.cpp +++ b/dom/ipc/WindowGlobalActor.cpp @@ -56,6 +56,7 @@ WindowGlobalInit WindowGlobalActor::BaseInitializer( auto& fields = ctx.mFields; fields.mEmbedderPolicy = InheritedPolicy(aBrowsingContext); fields.mAutoplayPermission = nsIPermissionManager::UNKNOWN_ACTION; + fields.mAllowJavascript = true; return init; } diff --git a/editor/composer/nsEditingSession.cpp b/editor/composer/nsEditingSession.cpp index 5720b56a28a1..c5726da3b71b 100644 --- a/editor/composer/nsEditingSession.cpp +++ b/editor/composer/nsEditingSession.cpp @@ -42,6 +42,7 @@ #include "nsStringFwd.h" // for nsString #include "mozilla/dom/BrowsingContext.h" // for BrowsingContext #include "mozilla/dom/Selection.h" // for AutoHideSelectionChanges, etc +#include "mozilla/dom/WindowContext.h" // for WindowContext #include "nsFrameSelection.h" // for nsFrameSelection #include "nsBaseCommandController.h" // for nsBaseCommandController #include "mozilla/dom/LoadURIOptionsBinding.h" @@ -120,7 +121,7 @@ nsEditingSession::MakeWindowEditable(mozIDOMWindowProxy* aWindow, nsresult rv; if (!mInteractive) { - rv = DisableJSAndPlugins(*docShell); + rv = DisableJSAndPlugins(window->GetCurrentInnerWindow()); NS_ENSURE_SUCCESS(rv, rv); } @@ -172,28 +173,25 @@ nsEditingSession::MakeWindowEditable(mozIDOMWindowProxy* aWindow, return rv; } -nsresult nsEditingSession::DisableJSAndPlugins(nsIDocShell& aDocShell) { - bool tmp; - nsresult rv = aDocShell.GetAllowJavascript(&tmp); - NS_ENSURE_SUCCESS(rv, rv); +nsresult nsEditingSession::DisableJSAndPlugins(nsPIDOMWindowInner* aWindow) { + WindowContext* wc = aWindow->GetWindowContext(); + BrowsingContext* bc = wc->GetBrowsingContext(); - mScriptsEnabled = tmp; + mScriptsEnabled = wc->GetAllowJavascript(); - rv = aDocShell.SetAllowJavascript(false); - NS_ENSURE_SUCCESS(rv, rv); + MOZ_TRY(wc->SetAllowJavascript(false)); // Disable plugins in this document: - mPluginsEnabled = aDocShell.PluginsAllowedInCurrentDoc(); + mPluginsEnabled = bc->GetAllowPlugins(); - rv = aDocShell.GetBrowsingContext()->SetAllowPlugins(false); - NS_ENSURE_SUCCESS(rv, rv); + MOZ_TRY(bc->SetAllowPlugins(false)); mDisabledJSAndPlugins = true; return NS_OK; } -nsresult nsEditingSession::RestoreJSAndPlugins(nsPIDOMWindowOuter* aWindow) { +nsresult nsEditingSession::RestoreJSAndPlugins(nsPIDOMWindowInner* aWindow) { if (!mDisabledJSAndPlugins) { return NS_OK; } @@ -204,17 +202,15 @@ nsresult nsEditingSession::RestoreJSAndPlugins(nsPIDOMWindowOuter* aWindow) { // DetachFromWindow may call this method with nullptr. return NS_ERROR_FAILURE; } - nsIDocShell* docShell = aWindow->GetDocShell(); - NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); - nsresult rv = docShell->SetAllowJavascript(mScriptsEnabled); - NS_ENSURE_SUCCESS(rv, rv); + WindowContext* wc = aWindow->GetWindowContext(); + BrowsingContext* bc = wc->GetBrowsingContext(); + + MOZ_TRY(wc->SetAllowJavascript(mScriptsEnabled)); // Disable plugins in this document: - auto* browsingContext = aWindow->GetBrowsingContext(); - NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE); - return browsingContext->SetAllowPlugins(mPluginsEnabled); + return bc->SetAllowPlugins(mPluginsEnabled); } /*--------------------------------------------------------------------------- @@ -510,7 +506,7 @@ nsEditingSession::TearDownEditorOnWindow(mozIDOMWindowProxy* aWindow) { if (stopEditing) { // Make things the way they were before we started editing. - RestoreJSAndPlugins(window); + RestoreJSAndPlugins(window->GetCurrentInnerWindow()); RestoreAnimationMode(window); if (mMakeWholeDocumentEditable) { @@ -1192,7 +1188,7 @@ nsresult nsEditingSession::DetachFromWindow(nsPIDOMWindowOuter* aWindow) { // make things the way they were before we started editing. RemoveEditorControllers(aWindow); RemoveWebProgressListener(aWindow); - RestoreJSAndPlugins(aWindow); + RestoreJSAndPlugins(aWindow->GetCurrentInnerWindow()); RestoreAnimationMode(aWindow); // Kill our weak reference to our original window, in case @@ -1219,7 +1215,7 @@ nsresult nsEditingSession::ReattachToWindow(nsPIDOMWindowOuter* aWindow) { // Disable plugins. if (!mInteractive) { - rv = DisableJSAndPlugins(*docShell); + rv = DisableJSAndPlugins(aWindow->GetCurrentInnerWindow()); NS_ENSURE_SUCCESS(rv, rv); } diff --git a/editor/composer/nsEditingSession.h b/editor/composer/nsEditingSession.h index 5dd9e4d262d9..c808c71c79af 100644 --- a/editor/composer/nsEditingSession.h +++ b/editor/composer/nsEditingSession.h @@ -31,6 +31,8 @@ class nsIChannel; class nsIControllers; class nsIDocShell; class nsIWebProgress; +class nsIPIDOMWindowOuter; +class nsIPIDOMWindowInner; namespace mozilla { class ComposerCommandsUpdater; @@ -113,13 +115,13 @@ class nsEditingSession final : public nsIEditingSession, /** * Disable scripts and plugins in aDocShell. */ - nsresult DisableJSAndPlugins(nsIDocShell& aDocShell); + nsresult DisableJSAndPlugins(nsPIDOMWindowInner* aWindow); /** * Restore JS and plugins (enable/disable them) according to the state they * were before the last call to disableJSAndPlugins. */ - nsresult RestoreJSAndPlugins(nsPIDOMWindowOuter* aWindow); + nsresult RestoreJSAndPlugins(nsPIDOMWindowInner* aWindow); protected: bool mDoneSetup; // have we prepared for editing yet? diff --git a/editor/composer/test/test_bug519928.html b/editor/composer/test/test_bug519928.html index cb5d7136131e..4ccb1aaba5b8 100644 --- a/editor/composer/test/test_bug519928.html +++ b/editor/composer/test/test_bug519928.html @@ -22,7 +22,7 @@ var iframe = document.getElementById("load-frame"); function enableJS() { allowJS(true, iframe); } function disableJS() { allowJS(false, iframe); } function allowJS(allow, frame) { - SpecialPowers.wrap(frame.contentWindow).docShell.allowJavascript = allow; + SpecialPowers.wrap(frame.contentWindow).windowGlobalChild.windowContext.allowJavascript = allow; } function expectJSAllowed(allowed, testCondition, callback) { diff --git a/js/xpconnect/src/XPCJSRuntime.cpp b/js/xpconnect/src/XPCJSRuntime.cpp index c680175969aa..ec3c3ac47a65 100644 --- a/js/xpconnect/src/XPCJSRuntime.cpp +++ b/js/xpconnect/src/XPCJSRuntime.cpp @@ -456,7 +456,7 @@ void NukeJSStackFrames(JS::Realm* aRealm) { Scriptability::Scriptability(JS::Realm* realm) : mScriptBlocks(0), - mDocShellAllowsScript(true), + mWindowAllowsScript(true), mScriptBlockedByPolicy(false) { nsIPrincipal* prin = nsJSPrincipals::get(JS::GetRealmPrincipals(realm)); @@ -477,7 +477,7 @@ Scriptability::Scriptability(JS::Realm* realm) } bool Scriptability::Allowed() { - return mDocShellAllowsScript && !mScriptBlockedByPolicy && mScriptBlocks == 0; + return mWindowAllowsScript && !mScriptBlockedByPolicy && mScriptBlocks == 0; } bool Scriptability::IsImmuneToScriptPolicy() { return mImmuneToScriptPolicy; } @@ -489,8 +489,8 @@ void Scriptability::Unblock() { --mScriptBlocks; } -void Scriptability::SetDocShellAllowsScript(bool aAllowed) { - mDocShellAllowsScript = aAllowed || mImmuneToScriptPolicy; +void Scriptability::SetWindowAllowsScript(bool aAllowed) { + mWindowAllowsScript = aAllowed || mImmuneToScriptPolicy; } /* static */ diff --git a/js/xpconnect/src/xpcpublic.h b/js/xpconnect/src/xpcpublic.h index dfef8ab06816..c059811b1bbd 100644 --- a/js/xpconnect/src/xpcpublic.h +++ b/js/xpconnect/src/xpcpublic.h @@ -81,7 +81,7 @@ class Scriptability { void Block(); void Unblock(); - void SetDocShellAllowsScript(bool aAllowed); + void SetWindowAllowsScript(bool aAllowed); static Scriptability& Get(JSObject* aScope); @@ -92,9 +92,9 @@ class Scriptability { // Script may not run if this value is non-zero. uint32_t mScriptBlocks; - // Whether the docshell allows javascript in this scope. If this scope - // doesn't have a docshell, this value is always true. - bool mDocShellAllowsScript; + // Whether the DOM window allows javascript in this scope. If this scope + // doesn't have a window, this value is always true. + bool mWindowAllowsScript; // Whether this scope is immune to user-defined or addon-defined script // policy. diff --git a/mobile/android/actors/GeckoViewSettingsChild.jsm b/mobile/android/actors/GeckoViewSettingsChild.jsm index eed2a413f3cc..9306512e10d5 100644 --- a/mobile/android/actors/GeckoViewSettingsChild.jsm +++ b/mobile/android/actors/GeckoViewSettingsChild.jsm @@ -28,7 +28,6 @@ class GeckoViewSettingsChild extends GeckoViewActorChild { case "SettingsUpdate": { const settings = message.data; - this.allowJavascript = settings.allowJavascript; this.viewportMode = settings.viewportMode; if (settings.isPopup) { // Allow web extensions to close their own action popups (bz1612363) @@ -38,14 +37,6 @@ class GeckoViewSettingsChild extends GeckoViewActorChild { } } - get allowJavascript() { - return this.docShell.allowJavascript; - } - - set allowJavascript(aAllowJavascript) { - this.docShell.allowJavascript = aAllowJavascript; - } - set viewportMode(aMode) { const { windowUtils } = this.contentWindow; if (aMode === windowUtils.desktopModeViewport) { diff --git a/mobile/android/modules/geckoview/GeckoViewSettings.jsm b/mobile/android/modules/geckoview/GeckoViewSettings.jsm index 8c01aa9d151e..f9169a891185 100644 --- a/mobile/android/modules/geckoview/GeckoViewSettings.jsm +++ b/mobile/android/modules/geckoview/GeckoViewSettings.jsm @@ -88,6 +88,14 @@ class GeckoViewSettings extends GeckoViewModule { ); } + get allowJavascript() { + return this.browsingContext.allowJavascript; + } + + set allowJavascript(aAllowJavascript) { + this.browsingContext.allowJavascript = aAllowJavascript; + } + get customUserAgent() { if (this.userAgentOverride !== null) { return this.userAgentOverride; diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html index 2e602b4db0e0..34b75933a037 100644 --- a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html @@ -111,7 +111,7 @@ async function starttest(){ // Play with the window object. var docShell = SpecialPowers.wrap(window).docShell; - ok(docShell.allowJavascript, "Able to pull properties off of docshell!"); + ok(docShell.browsingContext, "Able to pull properties off of docshell!"); // Make sure Xray-wrapped functions work. try { diff --git a/testing/modules/XPCShellContentUtils.jsm b/testing/modules/XPCShellContentUtils.jsm index 0585ed2215e7..87eb54154a2d 100644 --- a/testing/modules/XPCShellContentUtils.jsm +++ b/testing/modules/XPCShellContentUtils.jsm @@ -230,6 +230,10 @@ class ContentPage { return browser; } + get browsingContext() { + return this.browser.browsingContext; + } + sendMessage(msg, data) { return MessageChannel.sendMessage(this.browser.messageManager, msg, data); } diff --git a/toolkit/components/sessionstore/SessionStoreUtils.cpp b/toolkit/components/sessionstore/SessionStoreUtils.cpp index 5f0e2776a730..6ef92d152120 100644 --- a/toolkit/components/sessionstore/SessionStoreUtils.cpp +++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp @@ -38,6 +38,7 @@ #include "nsIFormControl.h" #include "nsIScrollableFrame.h" #include "nsISHistory.h" +#include "nsIXULRuntime.h" #include "nsPresContext.h" #include "nsPrintfCString.h" @@ -222,7 +223,6 @@ void SessionStoreUtils::CollectDocShellCapabilities(const GlobalObject& aGlobal, void SessionStoreUtils::RestoreDocShellCapabilities( nsIDocShell* aDocShell, const nsCString& aDisallowCapabilities) { aDocShell->SetAllowPlugins(true); - aDocShell->SetAllowJavascript(true); aDocShell->SetAllowMetaRedirects(true); aDocShell->SetAllowSubframes(true); aDocShell->SetAllowImages(true); @@ -232,12 +232,13 @@ void SessionStoreUtils::RestoreDocShellCapabilities( aDocShell->SetAllowContentRetargeting(true); aDocShell->SetAllowContentRetargetingOnChildren(true); + bool allowJavascript = true; for (const nsACString& token : nsCCharSeparatedTokenizer(aDisallowCapabilities, ',').ToRange()) { if (token.EqualsLiteral("Plugins")) { aDocShell->SetAllowPlugins(false); } else if (token.EqualsLiteral("Javascript")) { - aDocShell->SetAllowJavascript(false); + allowJavascript = false; } else if (token.EqualsLiteral("MetaRedirects")) { aDocShell->SetAllowMetaRedirects(false); } else if (token.EqualsLiteral("Subframes")) { @@ -261,6 +262,12 @@ void SessionStoreUtils::RestoreDocShellCapabilities( aDocShell->SetAllowContentRetargetingOnChildren(false); } } + + if (!mozilla::SessionHistoryInParent()) { + // With SessionHistoryInParent, this is set from the parent process. + BrowsingContext* bc = aDocShell->GetBrowsingContext(); + Unused << bc->SetAllowJavascript(allowJavascript); + } } static void CollectCurrentScrollPosition(JSContext* aCx, Document& aDocument, @@ -1463,6 +1470,16 @@ already_AddRefed SessionStoreUtils::RestoreDocShellState( } } + bool allowJavascript = true; + for (const nsACString& token : + nsCCharSeparatedTokenizer(aDocShellCaps, ',').ToRange()) { + if (token.EqualsLiteral("Javascript")) { + allowJavascript = false; + } + } + + Unused << aContext.SetAllowJavascript(allowJavascript); + DocShellRestoreState state = {uri, aDocShellCaps}; // TODO (anny): Investigate removing this roundtrip.