Bug 1646560: Part 2 - Move allowJavascript and friends from DocShell to BrowsingContext and WindowContext. r=jdescottes,nika,geckoview-reviewers,devtools-backward-compat-reviewers,agi
authorKris Maglione <maglione.k@gmail.com>
Tue, 15 Jun 2021 04:40:11 +0000
changeset 583096 0fa59c7f0f82c65ab24cf64ebe6aa758b90d50c0
parent 583095 284dc9ca5f5c47ed566f8909eaa736cf9bf36f42
child 583097 c892c376eb0ba164d7123f1da9d68beb3b325597
push id38540
push usersmolnar@mozilla.com
push dateTue, 15 Jun 2021 21:45:02 +0000
treeherdermozilla-central@206721f8064a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes, nika, geckoview-reviewers, devtools-backward-compat-reviewers, agi
bugs1646560
milestone91.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1646560: Part 2 - Move allowJavascript and friends from DocShell to BrowsingContext and WindowContext. r=jdescottes,nika,geckoview-reviewers,devtools-backward-compat-reviewers,agi This is slightly complicated by the fact that the editor code wants to be able to set this from the content process, so we really need separate BrowsingContext and WindowContext flags, the latter of which can be set by the owning process. Differential Revision: https://phabricator.services.mozilla.com/D114899
browser/components/sessionstore/test/browser_capabilities.js
caps/tests/mochitest/test_disableScript.xhtml
devtools/client/framework/test/browser_toolbox_options_disable_js.js
devtools/client/framework/toolbox-options.js
devtools/client/framework/toolbox.js
devtools/client/fronts/targets/browsing-context.js
devtools/server/actors/target-configuration.js
devtools/server/actors/targets/browsing-context.js
devtools/shared/commands/target-configuration/target-configuration-command.js
devtools/shared/specs/target-configuration.js
devtools/shared/specs/targets/browsing-context.js
docshell/base/BrowsingContext.cpp
docshell/base/BrowsingContext.h
docshell/base/WindowContext.cpp
docshell/base/WindowContext.h
docshell/base/nsDocShell.cpp
docshell/base/nsDocShell.h
docshell/base/nsIDocShell.idl
docshell/test/unit/AllowJavascriptChild.jsm
docshell/test/unit/AllowJavascriptParent.jsm
docshell/test/unit/test_allowJavascript.js
docshell/test/unit/xpcshell.ini
dom/base/nsGlobalWindowOuter.cpp
dom/chrome-webidl/BrowsingContext.webidl
dom/chrome-webidl/WindowGlobalActors.webidl
dom/ipc/WindowGlobalActor.cpp
editor/composer/nsEditingSession.cpp
editor/composer/nsEditingSession.h
editor/composer/test/test_bug519928.html
js/xpconnect/src/XPCJSRuntime.cpp
js/xpconnect/src/xpcpublic.h
mobile/android/actors/GeckoViewSettingsChild.jsm
mobile/android/modules/geckoview/GeckoViewSettings.jsm
testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html
testing/modules/XPCShellContentUtils.jsm
toolkit/components/sessionstore/SessionStoreUtils.cpp
--- a/browser/components/sessionstore/test/browser_capabilities.js
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -6,33 +6,33 @@
 /**
  * These tests ensures that disabling features by flipping nsIDocShell.allow*
  * properties are (re)stored as disabled. Disallowed features must be
  * re-enabled when the tab is re-used by another tab restoration.
  */
 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"));
 
   // Check that everything is allowed by default for new tabs.
   let state = JSON.parse(ss.getTabState(tab));
   ok(!("disallow" in state), "everything allowed by default");
   ok(
     flags.every(f => docShell[f]),
     "all flags set to true"
   );
 
   // 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.
   browser.reload();
   await promiseBrowserLoaded(browser);
 
   // Flush to make sure chrome received all data.
   await TabStateFlusher.flush(browser);
@@ -63,17 +63,17 @@ add_task(async function docshell_capabil
   // Restore the state with disallowed features.
   await promiseTabState(tab, disallowedState);
 
   // Check that docShell flags are set.
   ok(!docShell.allowImages, "images not allowed");
   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));
   disallow = new Set(state.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed anymore");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore");
   ok(!disallow.has("Javascript"), "Javascript still allowed");
   is(disallow.size, 2, "two capabilities disallowed");
--- a/caps/tests/mochitest/test_disableScript.xhtml
+++ b/caps/tests/mochitest/test_disableScript.xhtml
@@ -80,18 +80,18 @@ https://bugzilla.mozilla.org/show_bug.cg
   }
 
   function checkScriptEnabled(win, expectEnabled) {
     win.wrappedJSObject.gFiredOnclick = false;
     win.document.body.dispatchEvent(new win.Event('click'));
     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) {
     idx = idx || 0;
     return new Promise(resolve => {
       let target = list[idx] + path;
       info("Testing scriptability for: " + target + ". expecting " + expectEnabled);
       navigateFrame(win.frameElement, target).then(function() {
@@ -133,75 +133,75 @@ https://bugzilla.mozilla.org/show_bug.cg
   }
 
   function go() {
     var rootWin = rootFrame.contentWindow;
     var chromeWin = chromeFrame.contentWindow;
 
     // 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.
     addFrame(rootWin, 'parent', true).then(function() {
       checkScriptEnabled(rootWin[0], true);
       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);
 
     // Try navigating two frames, then munging docshell scriptability, then
     // pulling the frames out of the bfcache to make sure that flags are
     // properly propagated to inactive inner windows. We do this both for an
     // 'own' docshell, as well as for an ancestor docshell.
       return navigateFrame(rootWin[0][0].frameElement, rootWin[0][0].location + '-navigated');
     }).then(function() { return navigateFrame(rootWin[0][1][0].frameElement, rootWin[0][1][0].location + '-navigated'); })
       .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);
     }).then(function() { return navigateBack(rootWin[0][1][0].frameElement); })
       .then(function() {
       checkScriptEnabled(rootWin[0][0], false);
       checkScriptEnabled(rootWin[0][1][0], false);
 
--- a/devtools/client/framework/test/browser_toolbox_options_disable_js.js
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.js
@@ -29,18 +29,18 @@ add_task(async function() {
 
   await toolbox.destroy();
   gBrowser.removeCurrentTab();
 });
 
 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() {
     const doc = content.document;
     const output = doc.getElementById("output");
     doc.querySelector("#logJSEnabled").click();
     is(
       output.textContent,
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -600,20 +600,20 @@ OptionsPanel.prototype = {
     const prefName = "devtools.source-map.client-service.enabled";
     const enabled = GetPref(prefName);
     const box = this.panelDoc.querySelector(`[data-pref="${prefName}"]`);
     box.checked = enabled;
   },
 
   /**
    * 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.
    */
   _disableJSClicked: function(event) {
     const checked = event.target.checked;
 
     this.commands.targetConfigurationCommand.updateConfiguration({
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -2110,24 +2110,28 @@ Toolbox.prototype = {
     const pref = "devtools.serviceWorkers.testing.enabled";
     const serviceWorkersTestingEnabled = Services.prefs.getBoolPref(pref);
     this.commands.targetConfigurationCommand.updateConfiguration({
       serviceWorkersTestingEnabled,
     });
   },
 
   /**
-   * 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,
+      });
+    }
   },
 
   /**
    * Update the visibility of the buttons.
    */
   updateToolboxButtonsVisibility() {
     this.toolbarButtons.forEach(button => {
       button.isVisible = this._commandIsVisible(button);
--- a/devtools/client/fronts/targets/browsing-context.js
+++ b/devtools/client/fronts/targets/browsing-context.js
@@ -15,19 +15,18 @@ const { TargetMixin } = require("devtool
 class BrowsingContextTargetFront extends TargetMixin(
   FrontClassWithSpec(browsingContextTargetSpec)
 ) {
   constructor(client, targetFront, parentFront) {
     super(client, targetFront, parentFront);
 
     // 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.
     this._javascriptEnabled = null;
 
     this._onTabNavigated = this._onTabNavigated.bind(this);
     this._onFrameUpdate = this._onFrameUpdate.bind(this);
   }
@@ -116,38 +115,32 @@ class BrowsingContextTargetFront extends
       // translated on the target class. Listen for them before attaching as they
       // can start firing on attach call.
       this.on("tabNavigated", this._onTabNavigated);
       this.on("frameUpdate", this._onFrameUpdate);
 
       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) {
         await this.attachConsole();
       }
     })();
     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.
     // Avoid invalid loops and do not try to resolve only once the previous call to detach
     // is done as it would do async infinite loop that never resolves.
     if (this._isDetaching) {
       return;
     }
--- a/devtools/server/actors/target-configuration.js
+++ b/devtools/server/actors/target-configuration.js
@@ -188,16 +188,27 @@ const TargetConfigurationActor = ActorCl
     for (const [key, value] of Object.entries(configuration)) {
       switch (key) {
         case "colorSchemeSimulation":
           this._setColorSchemeSimulation(value);
           break;
         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;
         case "printSimulationEnabled":
           this._setPrintSimulationEnabled(value);
           break;
         case "rdmPaneMaxTouchPoints":
           this._setRDMPaneMaxTouchPoints(value);
@@ -238,16 +249,20 @@ const TargetConfigurationActor = ActorCl
     }
 
     // Restore the origin device pixel ratio only if it was explicitly updated by this
     // specific actor.
     if (this._initialDPPXOverride !== undefined) {
       this._setDPPXOverride(this._initialDPPXOverride);
     }
 
+    if (this._initialJavascriptEnabled !== undefined) {
+      this._setJavascriptEnabled(this._initialJavascriptEnabled);
+    }
+
     if (this._initialTouchEventsOverride !== undefined) {
       this._setTouchEventsOverride(this._initialTouchEventsOverride);
     }
   },
 
   /**
    * Disable or enable the service workers testing features.
    */
@@ -292,16 +307,28 @@ const TargetConfigurationActor = ActorCl
 
     if (this._initialUserAgent === undefined) {
       this._initialUserAgent = this._browsingContext.customUserAgent;
     }
 
     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) {
       return;
     }
 
     if (!dppx && this._initialDPPXOverride) {
       dppx = this._initialDPPXOverride;
--- a/devtools/server/actors/targets/browsing-context.js
+++ b/devtools/server/actors/targets/browsing-context.js
@@ -1085,17 +1085,16 @@ const browsingContextTargetPrototype = {
       };
     }
 
     this._attach();
 
     return {
       threadActor: this.threadActor.actorID,
       cacheDisabled: this._getCacheDisabled(),
-      javascriptEnabled: this._getJavascriptEnabled(),
       traits: this.traits,
     };
   },
 
   detach(request) {
     if (!this._detach()) {
       throw {
         error: "wrongState",
@@ -1273,24 +1272,16 @@ const browsingContextTargetPrototype = {
       }
     }
 
     if (!this.isTopLevelTarget) {
       // Following DevTools target options should only apply to the top target and be
       // 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()
     ) {
       this._setCacheDisabled(options.cacheDisabled);
     }
     if (
       typeof options.paintFlashing !== "undefined" &&
@@ -1315,17 +1306,16 @@ const browsingContextTargetPrototype = {
     return this._touchSimulator;
   },
 
   /**
    * Opposite of the updateTargetConfiguration method, that resets document
    * state when closing the toolbox.
    */
   _restoreTargetConfiguration() {
-    this._restoreJavascript();
     this._setCacheDisabled(false);
     this._setPaintFlashingEnabled(false);
 
     if (this._restoreFocus && this.browsingContext?.isActive) {
       this.window.focus();
     }
   },
 
@@ -1335,49 +1325,16 @@ const browsingContextTargetPrototype = {
   _setCacheDisabled(disabled) {
     const enable = Ci.nsIRequest.LOAD_NORMAL;
     const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE;
 
     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.
    */
   _setPaintFlashingEnabled(enabled) {
     const windowUtils = this.window.windowUtils;
     windowUtils.paintFlashing = enabled;
   },
 
   /**
--- a/devtools/shared/commands/target-configuration/target-configuration-command.js
+++ b/devtools/shared/commands/target-configuration/target-configuration-command.js
@@ -56,30 +56,33 @@ class TargetConfigurationCommand {
     } else {
       await this._commands.targetCommand.targetFront.reconfigure({
         options: configuration,
       });
     }
   }
 
   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;
   }
 
   /**
    * Change orientation type and angle (that can be accessed through screen.orientation in
    * the content page) and simulates the "orientationchange" event when the device screen
    * was rotated.
    * Note that this will only be effective if the Responsive Design Mode is enabled.
    *
--- a/devtools/shared/specs/target-configuration.js
+++ b/devtools/shared/specs/target-configuration.js
@@ -33,12 +33,18 @@ const targetConfigurationSpec = generate
     updateConfiguration: {
       request: {
         configuration: Arg(0, "target-configuration.configuration"),
       },
       response: {
         configuration: RetVal("target-configuration.configuration"),
       },
     },
+    isJavascriptEnabled: {
+      request: {},
+      response: {
+        javascriptEnabled: RetVal("boolean"),
+      },
+    },
   },
 });
 
 exports.targetConfigurationSpec = targetConfigurationSpec;
--- a/devtools/shared/specs/targets/browsing-context.js
+++ b/devtools/shared/specs/targets/browsing-context.js
@@ -9,17 +9,17 @@ const {
   RetVal,
   Option,
   Arg,
 } = require("devtools/shared/protocol");
 
 types.addDictType("browsingContextTarget.attach", {
   threadActor: "number",
   cacheDisabled: "boolean",
-  javascriptEnabled: "boolean",
+  javascriptEnabled: "nullable:boolean",
   traits: "json",
 });
 
 types.addDictType("browsingContextTarget.switchtoframe", {
   message: "string",
 });
 
 types.addDictType("browsingContextTarget.listframes", {
@@ -41,17 +41,16 @@ types.addDictType("browsingContextTarget
 types.addDictType("browsingContextTarget.reload", {
   force: "boolean",
 });
 
 // @backward-compat { version 87 } See backward-compat note for `reconfigure`.
 types.addDictType("browsingContextTarget.reconfigure", {
   cacheDisabled: "nullable:boolean",
   colorSchemeSimulation: "nullable:string",
-  javascriptEnabled: "nullable:boolean",
   paintFlashing: "nullable:boolean",
   printSimulationEnabled: "nullable:boolean",
   restoreFocus: "nullable:boolean",
   serviceWorkersTestingEnabled: "nullable:boolean",
 });
 
 const browsingContextTargetSpecPrototype = {
   typeName: "browsingContextTarget",
--- a/docshell/base/BrowsingContext.cpp
+++ b/docshell/base/BrowsingContext.cpp
@@ -401,16 +401,18 @@ already_AddRefed<BrowsingContext> Browsi
   fields.mOrientationLock = mozilla::hal::eScreenOrientation_None;
 
   fields.mUseGlobalHistory = inherit ? inherit->GetUseGlobalHistory() : false;
 
   fields.mUseErrorPages = true;
 
   fields.mTouchEventsOverrideInternal = TouchEventsOverride::None;
 
+  fields.mAllowJavascript = inherit ? inherit->GetAllowJavascript() : true;
+
   RefPtr<BrowsingContext> context;
   if (XRE_IsParentProcess()) {
     context = new CanonicalBrowsingContext(parentWC, group, id,
                                            /* aOwnerProcessId */ 0,
                                            /* aEmbedderProcessId */ 0, aType,
                                            std::move(fields));
   } else {
     context =
@@ -533,16 +535,17 @@ BrowsingContext::BrowsingContext(WindowC
       mIsDiscarded(false),
       mWindowless(false),
       mDanglingRemoteOuterProxies(false),
       mEmbeddedByThisProcess(false),
       mUseRemoteTabs(false),
       mUseRemoteSubframes(false),
       mCreatedDynamically(false),
       mIsInBFCache(false),
+      mCanExecuteScripts(true),
       mChildOffset(0) {
   MOZ_RELEASE_ASSERT(!mParentWindow || mParentWindow->Group() == mGroup);
   MOZ_RELEASE_ASSERT(mBrowsingContextId != 0);
   MOZ_RELEASE_ASSERT(mGroup);
 }
 
 void BrowsingContext::SetDocShell(nsIDocShell* aDocShell) {
   // XXX(nika): We should communicate that we are now an active BrowsingContext
@@ -737,16 +740,17 @@ void BrowsingContext::Attach(bool aFromI
       MOZ_DIAGNOSTIC_ASSERT(mParentWindow->GetWindowGlobalChild(),
                             "local attach call with oop parent window");
       MOZ_DIAGNOSTIC_ASSERT(mParentWindow->GetWindowGlobalChild()->CanSend(),
                             "local attach call with dead parent window");
     }
     mChildOffset =
         mCreatedDynamically ? -1 : mParentWindow->Children().Length();
     mParentWindow->AppendChildBrowsingContext(this);
+    RecomputeCanExecuteScripts();
   } else {
     mGroup->Toplevels().AppendElement(this);
   }
 
   if (GetIsPopupSpam()) {
     PopupBlocker::RegisterOpenPopupSpam();
   }
 
@@ -2623,16 +2627,53 @@ auto BrowsingContext::CanSet(FieldIndex<
 void BrowsingContext::DidSet(FieldIndex<IDX_HasMainMediaController>,
                              bool aOldValue) {
   if (!IsTop() || aOldValue == GetHasMainMediaController()) {
     return;
   }
   Group()->UpdateToplevelsSuspendedIfNeeded();
 }
 
+auto BrowsingContext::CanSet(FieldIndex<IDX_AllowJavascript>, 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<IDX_AllowJavascript>, 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;
   }
   // We should suspend a page only when it's inactive and doesn't have a main
   // media controller. Having a main controller in context means it might be
   // playing media, or waiting media keys to control media (could be not playing
   // anything currently)
--- a/docshell/base/BrowsingContext.h
+++ b/docshell/base/BrowsingContext.h
@@ -195,17 +195,20 @@ enum class ExplicitActiveStatus : uint8_
   /* True if the top level browsing context owns a main media controller */   \
   FIELD(HasMainMediaController, bool)                                         \
   /* The number of entries added to the session history because of this       \
    * browsing context. */                                                     \
   FIELD(HistoryEntryCount, uint32_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
 // particular the tree structure of nested browsing contexts is
 // represented by the tree of BrowsingContexts.
 //
 // The tree of BrowsingContexts is created in step with its
 // corresponding nsDocShell, and when nsDocShells are connected
@@ -846,30 +849,39 @@ class BrowsingContext : public nsILoadCo
   dom::PrefersColorSchemeOverride PrefersColorSchemeOverride() const {
     return GetPrefersColorSchemeOverride();
   }
 
   void FlushSessionStore();
 
   bool IsInBFCache() const { return mIsInBFCache; }
 
+  bool AllowJavascript() const { return GetAllowJavascript(); }
+  bool CanExecuteScripts() const { return mCanExecuteScripts; }
+
  protected:
   virtual ~BrowsingContext();
   BrowsingContext(WindowContext* aParentWindow, BrowsingContextGroup* aGroup,
                   uint64_t aBrowsingContextId, Type aType, FieldValues&& aInit);
 
   void SetChildSHistory(ChildSHistory* aChildSHistory);
   already_AddRefed<ChildSHistory> ForgetChildSHistory() {
     // FIXME Do we need to unset mHasSessionHistory?
     return mChildSessionHistory.forget();
   }
 
  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,
                                        BrowsingContext& aRequestingContext);
 
   // Is it early enough in the BrowsingContext's lifecycle that it is still
   // OK to set OriginAttributes?
   bool CanSetOriginAttributes();
@@ -1064,16 +1076,20 @@ class BrowsingContext : public nsILoadCo
 
   bool CanSet(FieldIndex<IDX_PendingInitialization>, bool aNewValue,
               ContentParent* aSource);
 
   CanSetResult CanSet(FieldIndex<IDX_HasMainMediaController>, bool aNewValue,
                       ContentParent* aSource);
   void DidSet(FieldIndex<IDX_HasMainMediaController>, bool aOldValue);
 
+  CanSetResult CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue,
+                      ContentParent* aSource);
+  void DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue);
+
   bool CanSet(FieldIndex<IDX_HasRestoreData>, bool aNewValue,
               ContentParent* aSource);
 
   template <size_t I, typename T>
   bool CanSet(FieldIndex<I>, const T&, ContentParent*) {
     return true;
   }
 
@@ -1180,16 +1196,21 @@ class BrowsingContext : public nsILoadCo
   // True if this BrowsingContext is for a frame that was added dynamically.
   bool mCreatedDynamically : 1;
 
   // Set to true if the browsing context is in the bfcache and pagehide has been
   // dispatched. When coming out from the bfcache, the value is set to false
   // 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;
 
   // The start time of user gesture, this is only available if the browsing
   // context is in process.
   TimeStamp mUserGestureStart;
 
--- a/docshell/base/WindowContext.cpp
+++ b/docshell/base/WindowContext.cpp
@@ -257,16 +257,54 @@ bool WindowContext::CanSet(FieldIndex<ID
   return CheckOnlyOwningProcessCanSet(aSource);
 }
 
 bool WindowContext::CanSet(FieldIndex<IDX_HadLazyLoadImage>, const bool& aValue,
                            ContentParent* aSource) {
   return IsTop();
 }
 
+bool WindowContext::CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue,
+                           ContentParent* aSource) {
+  return (XRE_IsParentProcess() && !aSource) || CheckOnlyOwningProcessCanSet(aSource);
+}
+
+void WindowContext::DidSet(FieldIndex<IDX_AllowJavascript>, 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<BrowsingContext>& child : Children()) {
+      child->RecomputeCanExecuteScripts();
+    }
+  }
+}
+
 void WindowContext::DidSet(FieldIndex<IDX_SHEntryHasUserInteraction>,
                            bool aOldValue) {
   MOZ_ASSERT(
       TopWindowContext() == this,
       "SHEntryHasUserInteraction can only be set on the top window context");
   // This field is set when the child notifies us of new user interaction, so we
   // also set the currently active shentry in the parent as having interaction.
   if (XRE_IsParentProcess() && mBrowsingContext) {
@@ -468,16 +506,17 @@ WindowContext::WindowContext(BrowsingCon
                              FieldValues&& aInit)
     : mFields(std::move(aInit)),
       mInnerWindowId(aInnerWindowId),
       mOuterWindowId(aOuterWindowId),
       mBrowsingContext(aBrowsingContext) {
   MOZ_ASSERT(mBrowsingContext);
   MOZ_ASSERT(mInnerWindowId);
   MOZ_ASSERT(mOuterWindowId);
+  RecomputeCanExecuteScripts(/* aApplyChanges */ false);
 }
 
 WindowContext::~WindowContext() {
   if (gWindowContexts) {
     gWindowContexts->Remove(InnerWindowId());
   }
 }
 
--- a/docshell/base/WindowContext.h
+++ b/docshell/base/WindowContext.h
@@ -26,72 +26,75 @@ class LogModule;
 namespace dom {
 
 class WindowGlobalChild;
 class WindowGlobalParent;
 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<uint32_t>)                               \
-  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<uint64_t>)                  \
-  /* 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<uint32_t>)                                 \
+  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<uint64_t>)                    \
+  /* 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)
 
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WindowContext)
 
  public:
@@ -177,16 +180,19 @@ class WindowContext : public nsISupports
   // activation and the transient user gesture activation had been consumed
   // successfully.
   bool ConsumeTransientUserGestureActivation();
 
   bool CanShowPopup();
 
   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);
   virtual ~WindowContext();
 
   virtual void Init();
 
  private:
@@ -266,43 +272,57 @@ class WindowContext : public nsISupports
   }
 
   bool CanSet(FieldIndex<IDX_IsLocalIP>, const bool& aValue,
               ContentParent* aSource);
 
   bool CanSet(FieldIndex<IDX_HadLazyLoadImage>, const bool& aValue,
               ContentParent* aSource);
 
+  bool CanSet(FieldIndex<IDX_AllowJavascript>, bool aValue,
+              ContentParent* aSource);
+  void DidSet(FieldIndex<IDX_AllowJavascript>, bool aOldValue);
+
   void DidSet(FieldIndex<IDX_HasReportedShadowDOMUsage>, bool aOldValue);
 
   void DidSet(FieldIndex<IDX_SHEntryHasUserInteraction>, bool aOldValue);
 
   // Overload `DidSet` to get notifications for a particular field being set.
   //
   // You can also overload the variant that gets the old value if you need it.
   template <size_t I>
   void DidSet(FieldIndex<I>) {}
   template <size_t I, typename T>
   void DidSet(FieldIndex<I>, T&& aOldValue) {}
   void DidSet(FieldIndex<IDX_UserActivationState>);
 
+  // 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<BrowsingContext> mBrowsingContext;
   WeakPtr<WindowGlobalChild> mWindowGlobalChild;
 
   // --- NEVER CHANGE `mChildren` DIRECTLY! ---
   // Changes to this list need to be synchronized to the list within our
   // `mBrowsingContext`, and should only be performed through the
   // `AppendChildBrowsingContext` and `RemoveChildBrowsingContext` methods.
   nsTArray<RefPtr<BrowsingContext>> mChildren;
 
   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;
 };
 
 using WindowContextTransaction = WindowContext::BaseTransaction;
 using WindowContextInitializer = WindowContext::IPCInitializer;
 using MaybeDiscardedWindowContext = MaybeDiscarded<WindowContext>;
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -388,31 +388,29 @@ nsDocShell::nsDocShell(BrowsingContext* 
       mMetaViewportOverride(nsIDocShell::META_VIEWPORT_OVERRIDE_NONE),
       mChannelToDisconnectOnPageHide(0),
       mCreatingDocument(false),
 #ifdef DEBUG
       mInEnsureScriptEnv(false),
 #endif
       mInitialized(false),
       mAllowSubframes(true),
-      mAllowJavascript(true),
       mAllowMetaRedirects(true),
       mAllowImages(true),
       mAllowMedia(true),
       mAllowDNSPrefetch(true),
       mAllowWindowControl(true),
       mCSSErrorReportingEnabled(false),
       mAllowAuth(mItemType == typeContent),
       mAllowKeywordFixup(false),
       mDisableMetaRefreshWhenInactive(false),
       mIsAppTab(false),
       mDeviceSizeIsPageSize(false),
       mWindowDraggingAllowed(false),
       mInFrameSwap(false),
-      mCanExecuteScripts(false),
       mFiredUnloadEvent(false),
       mEODForCurrentDocument(false),
       mURIResultedInDocument(false),
       mIsBeingDestroyed(false),
       mIsExecutingOnLoadHandler(false),
       mSavingOldViewer(false),
       mAffectPrivateSessionLifetime(true),
       mInvisible(false),
@@ -1745,44 +1743,29 @@ nsDocShell::GetAllowPlugins(bool* aAllow
 
 NS_IMETHODIMP
 nsDocShell::SetAllowPlugins(bool aAllowPlugins) {
   // XXX should enable or disable a plugin host
   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);
   *aEnabled = mCSSErrorReportingEnabled;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDocShell::SetCssErrorReportingEnabled(bool aEnabled) {
   mCSSErrorReportingEnabled = 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);
   return mBrowsingContext->GetUsePrivateBrowsing(aUsePrivateBrowsing);
 }
 
 void nsDocShell::NotifyPrivateBrowsingChanged() {
   MOZ_ASSERT(!mIsBeingDestroyed);
 
@@ -2648,60 +2631,16 @@ Maybe<ClientInfo> nsDocShell::GetInitial
 
   if (!doc || !doc->IsInitialDocument()) {
     return Maybe<ClientInfo>();
   }
 
   return innerWindow->GetClientInfo();
 }
 
-void nsDocShell::RecomputeCanExecuteScripts() {
-  bool old = mCanExecuteScripts;
-  RefPtr<nsDocShell> 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<nsDocShell*>(child)->RecomputeCanExecuteScripts();
-    }
-  }
-}
-
 nsresult nsDocShell::SetDocLoaderParent(nsDocLoader* aParent) {
   bool wasFrame = IsFrame();
 
   nsresult rv = nsDocLoader::SetDocLoaderParent(aParent);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsISupportsPriority> priorityGroup = do_QueryInterface(mLoadGroup);
   if (wasFrame != IsFrame() && priorityGroup) {
@@ -2712,20 +2651,16 @@ nsresult nsDocShell::SetDocLoaderParent(
   nsISupports* parent = GetAsSupports(aParent);
 
   // If parent is another docshell, we inherit all their flags for
   // allowing plugins, scripting etc.
   bool value;
   nsCOMPtr<nsIDocShell> parentAsDocShell(do_QueryInterface(parent));
 
   if (parentAsDocShell) {
-    if (mAllowJavascript &&
-        NS_SUCCEEDED(parentAsDocShell->GetAllowJavascript(&value))) {
-      SetAllowJavascript(value);
-    }
     if (mAllowMetaRedirects &&
         NS_SUCCEEDED(parentAsDocShell->GetAllowMetaRedirects(&value))) {
       SetAllowMetaRedirects(value);
     }
     if (mAllowSubframes &&
         NS_SUCCEEDED(parentAsDocShell->GetAllowSubframes(&value))) {
       SetAllowSubframes(value);
     }
@@ -2750,19 +2685,16 @@ nsresult nsDocShell::SetDocLoaderParent(
     // like this that might be embedded within it.
   }
 
   nsCOMPtr<nsIURIContentListener> parentURIListener(do_GetInterface(parent));
   if (parentURIListener) {
     mContentListener->SetParentContentListener(parentURIListener);
   }
 
-  // Our parent has changed. Recompute scriptability.
-  RecomputeCanExecuteScripts();
-
   // Inform windows when they're being removed from their parent.
   if (!aParent) {
     MaybeClearStorageAccessFlag();
   }
 
   return NS_OK;
 }
 
@@ -2977,26 +2909,16 @@ nsDocShell::SetTreeOwner(nsIDocShellTree
       MOZ_RELEASE_ASSERT(
           oldBrowserChild == newBrowserChild,
           "Cannot change BrowserChild during nsDocShell lifetime!");
     } else {
       mBrowserChild = do_GetWeakReference(newBrowserChild);
     }
   }
 
-  // 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;
 }
 
 NS_IMETHODIMP
 nsDocShell::GetHistoryID(nsID& aID) {
   aID = mBrowsingContext->GetHistoryID();
   return NS_OK;
 }
@@ -7653,19 +7575,16 @@ nsresult nsDocShell::RestoreFromHistory(
 
   // Now we simulate appending child docshells for subframes.
   for (i = 0; i < childShells.Count(); ++i) {
     nsIDocShellTreeItem* childItem = childShells.ObjectAt(i);
     nsCOMPtr<nsIDocShell> childShell = do_QueryInterface(childItem);
 
     // 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);
 
     bool allowSubframes;
     childShell->GetAllowSubframes(&allowSubframes);
 
     bool allowImages;
     childShell->GetAllowImages(&allowImages);
@@ -7679,17 +7598,16 @@ nsresult nsDocShell::RestoreFromHistory(
     bool allowContentRetargetingOnChildren =
         childShell->GetAllowContentRetargetingOnChildren();
 
     // this.AddChild(child) calls child.SetDocLoaderParent(this), meaning that
     // the child inherits our state. Among other things, this means that the
     // child inherits our mPrivateBrowsingId, which is what we want.
     AddChild(childItem);
 
-    childShell->SetAllowJavascript(allowJavascript);
     childShell->SetAllowMetaRedirects(allowRedirects);
     childShell->SetAllowSubframes(allowSubframes);
     childShell->SetAllowImages(allowImages);
     childShell->SetAllowMedia(allowMedia);
     childShell->SetAllowDNSPrefetch(allowDNSPrefetch);
     childShell->SetAllowContentRetargeting(allowContentRetargeting);
     childShell->SetAllowContentRetargetingOnChildren(
         allowContentRetargetingOnChildren);
@@ -13086,22 +13004,16 @@ NS_IMETHODIMP nsDocShell::ExitPrintPrevi
 #if NS_PRINT_PREVIEW
   nsCOMPtr<nsIWebBrowserPrint> viewer = do_QueryInterface(mContentViewer);
   return viewer->ExitPrintPreview();
 #else
   return NS_OK;
 #endif
 }
 
-NS_IMETHODIMP
-nsDocShell::GetCanExecuteScripts(bool* aResult) {
-  *aResult = mCanExecuteScripts;
-  return NS_OK;
-}
-
 /* [infallible] */
 NS_IMETHODIMP nsDocShell::GetIsTopLevelContentDocShell(
     bool* aIsTopLevelContentDocShell) {
   *aIsTopLevelContentDocShell = false;
 
   if (mItemType == typeContent) {
     *aIsTopLevelContentDocShell = mBrowsingContext->IsTopContent();
   }
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -947,17 +947,16 @@ class nsDocShell final : public nsDocLoa
   void FirePageHideShowNonRecursive(bool aShow);
 
   nsresult Dispatch(mozilla::TaskCategory aCategory,
                     already_AddRefed<nsIRunnable>&& aRunnable);
 
   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);
   void UpdateGlobalHistoryTitle(nsIURI* aURI);
   bool IsFrame() { return mBrowsingContext->IsFrame(); }
   bool CanSetOriginAttributes();
   bool ShouldBlockLoadingForBackButton();
   static bool ShouldDiscardLayoutState(nsIHttpChannel* aChannel);
@@ -1240,36 +1239,30 @@ class nsDocShell final : public nsDocLoa
   bool mCreatingDocument;  // (should be) debugging only
 #ifdef DEBUG
   bool mInEnsureScriptEnv;
   uint64_t mDocShellID = 0;
 #endif
 
   bool mInitialized : 1;
   bool mAllowSubframes : 1;
-  bool mAllowJavascript : 1;
   bool mAllowMetaRedirects : 1;
   bool mAllowImages : 1;
   bool mAllowMedia : 1;
   bool mAllowDNSPrefetch : 1;
   bool mAllowWindowControl : 1;
   bool mCSSErrorReportingEnabled : 1;
   bool mAllowAuth : 1;
   bool mAllowKeywordFixup : 1;
   bool mDisableMetaRefreshWhenInactive : 1;
   bool mIsAppTab : 1;
   bool mDeviceSizeIsPageSize : 1;
   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.
   bool mFiredUnloadEvent : 1;
 
   // this flag is for bug #21358. a docshell may load many urls
   // which don't result in new documents being created (i.e. a new
   // content viewer) we want to make sure we don't call a on load
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -188,21 +188,16 @@ interface nsIDocShell : nsIDocShellTreeI
   attribute boolean cssErrorReportingEnabled;
 
   /**
    * Whether to allow plugin execution
    */
   attribute boolean allowPlugins;
 
   /**
-   * Whether to allow Javascript execution
-   */
-  attribute boolean allowJavascript;
-
-  /**
    * Attribute stating if refresh based redirects can be allowed
    */
   attribute boolean allowMetaRedirects;
 
   /**
    * Attribute stating if it should allow subframes (framesets/iframes) or not
    */
   attribute boolean allowSubframes;
@@ -464,23 +459,16 @@ interface nsIDocShell : nsIDocShellTreeI
 
   /**
    * Propagated to the print preview document viewer.  Must only be called on
    * a document viewer that has been initialized for print preview.
    */
   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.
    */
   readonly attribute nsIDRef historyID;
 
   /**
    * Helper method for accessing this value from C++
    */
   [noscript, notxpcom] nsIDRef HistoryID();
new file mode 100644
--- /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;
+  }
+}
new file mode 100644
--- /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;
+  }
+}
new file mode 100644
--- /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`<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <script type="application/javascript">
+    "use strict";
+    var gFiredOnload = false;
+    var gFiredOnclick = false;
+  </script>
+</head>
+<body onload="gFiredOnload = true;" onclick="gFiredOnclick = true;">
+</body>
+</html>`;
+
+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();
+});
--- a/docshell/test/unit/xpcshell.ini
+++ b/docshell/test/unit/xpcshell.ini
@@ -1,11 +1,16 @@
 [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]
 # Disabled for 1563343 -- URI fixup should be done at the app level in GV.
 skip-if = os == 'android'
 [test_URIFixup_search.js]
 skip-if = os == 'android'
 [test_URIFixup_info.js]
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -1842,17 +1842,17 @@ NS_DEFINE_STATIC_IID_ACCESSOR(WindowStat
 WindowStateHolder::WindowStateHolder(nsGlobalWindowInner* aWindow)
     : mInnerWindow(aWindow),
       mInnerWindowReflector(RootingCx(), aWindow->GetWrapper()) {
   MOZ_ASSERT(aWindow, "null window");
 
   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() {
   if (mInnerWindow) {
     // This window was left in the bfcache and is now going away. We need to
     // free it up.
     // Note that FreeInnerObjects may already have been called on the
     // inner window if its outer has already had SetDocShell(null)
@@ -2326,20 +2326,21 @@ nsresult nsGlobalWindowOuter::SetNewDocu
     JSAutoRealm ar(cx, GetWrapperPreserveColor());
 
     {
       JS::Rooted<JSObject*> outer(cx, GetWrapperPreserveColor());
       js::SetWindowProxy(cx, newInnerGlobal, outer);
       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
       // have to do this here, not in binding code, because this has to happen
       // after we've created the outer window proxy and stashed it in the outer
       // nsGlobalWindowOuter, so GetWrapperPreserveColor() on that outer
       // nsGlobalWindowOuter doesn't return null and
       // nsGlobalWindowOuter::OuterObject works correctly.
--- a/dom/chrome-webidl/BrowsingContext.webidl
+++ b/dom/chrome-webidl/BrowsingContext.webidl
@@ -189,16 +189,26 @@ interface BrowsingContext {
 
   /**
    * This allows chrome to override the default choice of whether touch events
    * are available in a specific BrowsingContext and its descendents.
    */
   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.
    */
   [NewObject, Throws]
   readonly attribute any historyID;
 
   readonly attribute ChildSHistory? childSessionHistory;
 
   // Resets the location change rate limit. Used for testing.
--- a/dom/chrome-webidl/WindowGlobalActors.webidl
+++ b/dom/chrome-webidl/WindowGlobalActors.webidl
@@ -24,16 +24,26 @@ interface WindowContext {
   readonly attribute boolean hasBeforeUnload;
 
   // True if the principal of this window is for a local ip address.
   readonly attribute boolean isLocalIP;
 
   // 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.
 enum PermitUnloadAction {
   "prompt",
   "dontUnload",
   "unload",
 };
--- a/dom/ipc/WindowGlobalActor.cpp
+++ b/dom/ipc/WindowGlobalActor.cpp
@@ -51,16 +51,17 @@ WindowGlobalInit WindowGlobalActor::Base
   ctx.mOuterWindowId = aOuterWindowId;
   ctx.mBrowsingContextId = aBrowsingContext->Id();
 
   // If any synced fields need to be initialized from our BrowsingContext, we
   // can initialize them here.
   auto& fields = ctx.mFields;
   fields.mEmbedderPolicy = InheritedPolicy(aBrowsingContext);
   fields.mAutoplayPermission = nsIPermissionManager::UNKNOWN_ACTION;
+  fields.mAllowJavascript = true;
   return init;
 }
 
 WindowGlobalInit WindowGlobalActor::AboutBlankInitializer(
     dom::BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal) {
   WindowGlobalInit init =
       BaseInitializer(aBrowsingContext, nsContentUtils::GenerateWindowId(),
                       nsContentUtils::GenerateWindowId());
--- a/editor/composer/nsEditingSession.cpp
+++ b/editor/composer/nsEditingSession.cpp
@@ -37,16 +37,17 @@
 #include "nsIWebProgress.h"               // for nsIWebProgress, etc
 #include "nsLiteralString.h"              // for NS_LITERAL_STRING
 #include "nsPIDOMWindow.h"                // for nsPIDOMWindow
 #include "nsPresContext.h"                // for nsPresContext
 #include "nsReadableUtils.h"              // for AppendUTF16toUTF8
 #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"
 
 class nsISupports;
 class nsIURI;
 
 using namespace mozilla;
@@ -115,17 +116,17 @@ nsEditingSession::MakeWindowEditable(moz
   NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
   mDocShell = do_GetWeakReference(docShell);
 
   mInteractive = aInteractive;
   mMakeWholeDocumentEditable = aMakeWholeDocumentEditable;
 
   nsresult rv;
   if (!mInteractive) {
-    rv = DisableJSAndPlugins(*docShell);
+    rv = DisableJSAndPlugins(window->GetCurrentInnerWindow());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // Always remove existing editor
   TearDownEditorOnWindow(aWindow);
 
   // Tells embedder that startup is in progress
   mEditorStatus = eEditorCreationInProgress;
@@ -167,59 +168,54 @@ nsEditingSession::MakeWindowEditable(moz
     //  it IS ok to destroy current editor
     if (NS_FAILED(rv)) {
       TearDownEditorOnWindow(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;
   }
 
   mDisabledJSAndPlugins = false;
 
   if (NS_WARN_IF(!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);
 }
 
 /*---------------------------------------------------------------------------
 
   WindowIsEditable
 
   boolean windowIsEditable (in nsIDOMWindow aWindow);
 ----------------------------------------------------------------------------*/
@@ -505,17 +501,17 @@ nsEditingSession::TearDownEditorOnWindow
   // Null out the editor on the docShell to trigger PreDestroy which
   // needs to happen before document state listeners are removed below.
   docShell->SetEditor(nullptr);
 
   RemoveListenersAndControllers(window, htmlEditor);
 
   if (stopEditing) {
     // Make things the way they were before we started editing.
-    RestoreJSAndPlugins(window);
+    RestoreJSAndPlugins(window->GetCurrentInnerWindow());
     RestoreAnimationMode(window);
 
     if (mMakeWholeDocumentEditable) {
       doc->SetEditableFlag(false);
       doc->SetEditingState(Document::EditingState::eOff);
     }
   }
 
@@ -1187,17 +1183,17 @@ nsresult nsEditingSession::DetachFromWin
     mLoadBlankDocTimer->Cancel();
     mLoadBlankDocTimer = nullptr;
   }
 
   // Remove controllers, webprogress listener, and otherwise
   // 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
   // it changes on restore, or otherwise dies.
   mDocShell = nullptr;
 
   return NS_OK;
 }
@@ -1214,17 +1210,17 @@ nsresult nsEditingSession::ReattachToWin
   nsresult rv;
 
   nsIDocShell* docShell = aWindow->GetDocShell();
   NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
   mDocShell = do_GetWeakReference(docShell);
 
   // Disable plugins.
   if (!mInteractive) {
-    rv = DisableJSAndPlugins(*docShell);
+    rv = DisableJSAndPlugins(aWindow->GetCurrentInnerWindow());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // Tells embedder that startup is in progress.
   mEditorStatus = eEditorCreationInProgress;
 
   // Adds back web progress listener.
   rv = PrepareForEditing(aWindow);
--- a/editor/composer/nsEditingSession.h
+++ b/editor/composer/nsEditingSession.h
@@ -26,16 +26,18 @@ class mozIDOMWindowProxy;
 class nsBaseCommandController;
 class nsIDOMWindow;
 class nsISupports;
 class nsITimer;
 class nsIChannel;
 class nsIControllers;
 class nsIDocShell;
 class nsIWebProgress;
+class nsIPIDOMWindowOuter;
+class nsIPIDOMWindowInner;
 
 namespace mozilla {
 class ComposerCommandsUpdater;
 class HTMLEditor;
 }  // namespace mozilla
 
 class nsEditingSession final : public nsIEditingSession,
                                public nsIWebProgressListener,
@@ -108,23 +110,23 @@ class nsEditingSession final : public ns
   void RemoveWebProgressListener(nsPIDOMWindowOuter* aWindow);
   void RestoreAnimationMode(nsPIDOMWindowOuter* aWindow);
   void RemoveListenersAndControllers(nsPIDOMWindowOuter* aWindow,
                                      mozilla::HTMLEditor* aHTMLEditor);
 
   /**
    * 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?
 
   // Used to prevent double creation of editor because nsIWebProgressListener
   //  receives a STATE_STOP notification before the STATE_START
   //  for our document, so we wait for the STATE_START, then STATE_STOP
   //  before creating an editor
--- a/editor/composer/test/test_bug519928.html
+++ b/editor/composer/test/test_bug519928.html
@@ -17,17 +17,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 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) {
   window.ICanRunMyJS = false;
   var self_ = window;
   testCondition();
 
   var doc = iframe.contentDocument;
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -451,17 +451,17 @@ void NukeJSStackFrames(JS::Realm* aRealm
     return;
   }
 
   realmPrivate->NukeJSStackFrames();
 }
 
 Scriptability::Scriptability(JS::Realm* realm)
     : mScriptBlocks(0),
-      mDocShellAllowsScript(true),
+      mWindowAllowsScript(true),
       mScriptBlockedByPolicy(false) {
   nsIPrincipal* prin = nsJSPrincipals::get(JS::GetRealmPrincipals(realm));
 
   mImmuneToScriptPolicy = PrincipalImmuneToScriptPolicy(prin);
   if (mImmuneToScriptPolicy) {
     return;
   }
   // If we're not immune, we should have a real principal with a URI.
@@ -472,30 +472,30 @@ Scriptability::Scriptability(JS::Realm* 
     mScriptBlockedByPolicy = !policyAllows;
     return;
   }
   // Something went wrong - be safe and block script.
   mScriptBlockedByPolicy = true;
 }
 
 bool Scriptability::Allowed() {
-  return mDocShellAllowsScript && !mScriptBlockedByPolicy && mScriptBlocks == 0;
+  return mWindowAllowsScript && !mScriptBlockedByPolicy && mScriptBlocks == 0;
 }
 
 bool Scriptability::IsImmuneToScriptPolicy() { return mImmuneToScriptPolicy; }
 
 void Scriptability::Block() { ++mScriptBlocks; }
 
 void Scriptability::Unblock() {
   MOZ_ASSERT(mScriptBlocks > 0);
   --mScriptBlocks;
 }
 
-void Scriptability::SetDocShellAllowsScript(bool aAllowed) {
-  mDocShellAllowsScript = aAllowed || mImmuneToScriptPolicy;
+void Scriptability::SetWindowAllowsScript(bool aAllowed) {
+  mWindowAllowsScript = aAllowed || mImmuneToScriptPolicy;
 }
 
 /* static */
 Scriptability& Scriptability::Get(JSObject* aScope) {
   return RealmPrivate::Get(aScope)->scriptability;
 }
 
 bool IsUAWidgetCompartment(JS::Compartment* compartment) {
--- a/js/xpconnect/src/xpcpublic.h
+++ b/js/xpconnect/src/xpcpublic.h
@@ -76,30 +76,30 @@ namespace xpc {
 class Scriptability {
  public:
   explicit Scriptability(JS::Realm* realm);
   bool Allowed();
   bool IsImmuneToScriptPolicy();
 
   void Block();
   void Unblock();
-  void SetDocShellAllowsScript(bool aAllowed);
+  void SetWindowAllowsScript(bool aAllowed);
 
   static Scriptability& Get(JSObject* aScope);
 
  private:
   // Whenever a consumer wishes to prevent script from running on a global,
   // it increments this value with a call to Block(). When it wishes to
   // re-enable it (if ever), it decrements this value with a call to Unblock().
   // 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.
   bool mImmuneToScriptPolicy;
 
   // Whether the new-style domain policy when this compartment was created
   // forbids script execution.
   bool mScriptBlockedByPolicy;
--- a/mobile/android/actors/GeckoViewSettingsChild.jsm
+++ b/mobile/android/actors/GeckoViewSettingsChild.jsm
@@ -23,34 +23,25 @@ class GeckoViewSettingsChild extends Gec
   receiveMessage(message) {
     const { name } = message;
     debug`receiveMessage: ${name}`;
 
     switch (name) {
       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)
           this.contentWindow.windowUtils.allowScriptsToClose();
         }
       }
     }
   }
 
-  get allowJavascript() {
-    return this.docShell.allowJavascript;
-  }
-
-  set allowJavascript(aAllowJavascript) {
-    this.docShell.allowJavascript = aAllowJavascript;
-  }
-
   set viewportMode(aMode) {
     const { windowUtils } = this.contentWindow;
     if (aMode === windowUtils.desktopModeViewport) {
       return;
     }
     windowUtils.desktopModeViewport = aMode === VIEWPORT_MODE_DESKTOP;
   }
 }
--- a/mobile/android/modules/geckoview/GeckoViewSettings.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewSettings.jsm
@@ -83,16 +83,24 @@ class GeckoViewSettings extends GeckoVie
     // When the page is loading from the main process (e.g. from an extension
     // page) we won't be able to query the actor here.
     this.getActor("GeckoViewSettings")?.sendAsyncMessage(
       "SettingsUpdate",
       settings
     );
   }
 
+  get allowJavascript() {
+    return this.browsingContext.allowJavascript;
+  }
+
+  set allowJavascript(aAllowJavascript) {
+    this.browsingContext.allowJavascript = aAllowJavascript;
+  }
+
   get customUserAgent() {
     if (this.userAgentOverride !== null) {
       return this.userAgentOverride;
     }
     if (this.userAgentMode === USER_AGENT_MODE_DESKTOP) {
       return DESKTOP_USER_AGENT;
     }
     if (this.userAgentMode === USER_AGENT_MODE_VR) {
--- a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html
+++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersExtension.html
@@ -106,17 +106,17 @@ async function starttest(){
                              .createInstance(SpecialPowers.Ci.nsIURIMutator)
                              .setSpec("http://www.foobar.org/")
                              .finalize();
   is(testURI.spec, "http://www.foobar.org/", "Getters/Setters should work correctly");
   is(SpecialPowers.wrap(document).getElementsByTagName('details').length, 0, "Should work with proxy-based DOM bindings.");
 
   // 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 {
     SpecialPowers.wrap(SpecialPowers.Components).ID('{00000000-0000-0000-0000-000000000000}');
     ok(true, "Didn't throw");
   }
   catch (e) {
     ok(false, "Threw while trying to call Xray-wrapped function.");
--- a/testing/modules/XPCShellContentUtils.jsm
+++ b/testing/modules/XPCShellContentUtils.jsm
@@ -225,16 +225,20 @@ class ContentPage {
 
     this.browser = browser;
 
     this.loadFrameScript(frameScript);
 
     return browser;
   }
 
+  get browsingContext() {
+    return this.browser.browsingContext;
+  }
+
   sendMessage(msg, data) {
     return MessageChannel.sendMessage(this.browser.messageManager, msg, data);
   }
 
   loadFrameScript(func) {
     let frameScript = `data:text/javascript,(${encodeURI(func)}).call(this)`;
     this.browser.messageManager.loadFrameScript(frameScript, true, true);
   }
--- a/toolkit/components/sessionstore/SessionStoreUtils.cpp
+++ b/toolkit/components/sessionstore/SessionStoreUtils.cpp
@@ -33,16 +33,17 @@
 #include "nsContentList.h"
 #include "nsContentUtils.h"
 #include "nsFocusManager.h"
 #include "nsGlobalWindowOuter.h"
 #include "nsIDocShell.h"
 #include "nsIFormControl.h"
 #include "nsIScrollableFrame.h"
 #include "nsISHistory.h"
+#include "nsIXULRuntime.h"
 #include "nsPresContext.h"
 #include "nsPrintfCString.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::dom::sessionstore;
 
 namespace {
@@ -217,32 +218,32 @@ void SessionStoreUtils::CollectDocShellC
   TRY_ALLOWPROP(ContentRetargetingOnChildren);
 #undef TRY_ALLOWPROP
 }
 
 /* static */
 void SessionStoreUtils::RestoreDocShellCapabilities(
     nsIDocShell* aDocShell, const nsCString& aDisallowCapabilities) {
   aDocShell->SetAllowPlugins(true);
-  aDocShell->SetAllowJavascript(true);
   aDocShell->SetAllowMetaRedirects(true);
   aDocShell->SetAllowSubframes(true);
   aDocShell->SetAllowImages(true);
   aDocShell->SetAllowMedia(true);
   aDocShell->SetAllowDNSPrefetch(true);
   aDocShell->SetAllowWindowControl(true);
   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")) {
       aDocShell->SetAllowSubframes(false);
     } else if (token.EqualsLiteral("Images")) {
       aDocShell->SetAllowImages(false);
     } else if (token.EqualsLiteral("Media")) {
       aDocShell->SetAllowMedia(false);
@@ -256,16 +257,22 @@ void SessionStoreUtils::RestoreDocShellC
       aDocShell->SetAllowContentRetargeting(
           false);  // will also set AllowContentRetargetingOnChildren
       aDocShell->SetAllowContentRetargetingOnChildren(
           allow);  // restore the allowProp to original
     } else if (token.EqualsLiteral("ContentRetargetingOnChildren")) {
       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,
                                          Nullable<CollectedData>& aRetVal) {
   PresShell* presShell = aDocument.GetPresShell();
   if (!presShell) {
     return;
   }
@@ -1458,16 +1465,26 @@ already_AddRefed<Promise> SessionStoreUt
     nsCOMPtr<nsIURI> uri;
     if (!aURL.IsEmpty()) {
       if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), aURL))) {
         aError.Throw(NS_ERROR_FAILURE);
         return nullptr;
       }
     }
 
+    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.
     wgp->SendRestoreDocShellState(state)->Then(
         GetMainThreadSerialEventTarget(), __func__,
         [promise](void) { promise->MaybeResolveWithUndefined(); },
         [promise](void) { promise->MaybeRejectWithUndefined(); });