summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/target-configuration.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/target-configuration.js')
-rw-r--r--devtools/server/actors/target-configuration.js493
1 files changed, 493 insertions, 0 deletions
diff --git a/devtools/server/actors/target-configuration.js b/devtools/server/actors/target-configuration.js
new file mode 100644
index 0000000000..35340ee668
--- /dev/null
+++ b/devtools/server/actors/target-configuration.js
@@ -0,0 +1,493 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol.js");
+const {
+ targetConfigurationSpec,
+} = require("resource://devtools/shared/specs/target-configuration.js");
+
+const {
+ SessionDataHelpers,
+} = require("resource://devtools/server/actors/watcher/SessionDataHelpers.jsm");
+const { isBrowsingContextPartOfContext } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs"
+);
+const { SUPPORTED_DATA } = SessionDataHelpers;
+const { TARGET_CONFIGURATION } = SUPPORTED_DATA;
+
+// List of options supported by this target configuration actor.
+/* eslint sort-keys: "error" */
+const SUPPORTED_OPTIONS = {
+ // Disable network request caching.
+ cacheDisabled: true,
+ // Enable color scheme simulation.
+ colorSchemeSimulation: true,
+ // Enable custom formatters
+ customFormatters: true,
+ // Set a custom user agent
+ customUserAgent: true,
+ // Enable JavaScript
+ javascriptEnabled: true,
+ // Force a custom device pixel ratio (used in RDM). Set to null to restore origin ratio.
+ overrideDPPX: true,
+ // Enable print simulation mode.
+ printSimulationEnabled: true,
+ // Override navigator.maxTouchPoints (used in RDM and doesn't apply if RDM isn't enabled)
+ rdmPaneMaxTouchPoints: true,
+ // Page orientation (used in RDM and doesn't apply if RDM isn't enabled)
+ rdmPaneOrientation: true,
+ // Enable allocation tracking, if set, contains an object defining the tracking configurations
+ recordAllocations: true,
+ // Reload the page when the touch simulation state changes (only works alongside touchEventsOverride)
+ reloadOnTouchSimulationToggle: true,
+ // Restore focus in the page after closing DevTools.
+ restoreFocus: true,
+ // Enable service worker testing over HTTP (instead of HTTPS only).
+ serviceWorkersTestingEnabled: true,
+ // Set the current tab offline
+ setTabOffline: true,
+ // Enable touch events simulation
+ touchEventsOverride: true,
+ // Used to configure and start/stop the JavaScript tracer
+ tracerOptions: true,
+ // Use simplified highlighters when prefers-reduced-motion is enabled.
+ useSimpleHighlightersForReducedMotion: true,
+};
+/* eslint-disable sort-keys */
+
+/**
+ * This actor manages the configuration flags which apply to DevTools targets.
+ *
+ * Configuration flags should be applied to all concerned targets when the
+ * configuration is updated, and new targets should also be able to read the
+ * flags when they are created. The flags will be forwarded to the WatcherActor
+ * and stored as TARGET_CONFIGURATION data entries.
+ * Some flags will be set directly set from this actor, in the parent process
+ * (see _updateParentProcessConfiguration), and others will be set from the target actor,
+ * in the content process.
+ *
+ * @constructor
+ *
+ */
+class TargetConfigurationActor extends Actor {
+ constructor(watcherActor) {
+ super(watcherActor.conn, targetConfigurationSpec);
+ this.watcherActor = watcherActor;
+
+ this._onBrowsingContextAttached =
+ this._onBrowsingContextAttached.bind(this);
+ // We need to be notified of new browsing context being created so we can re-set flags
+ // we already set on the "previous" browsing context. We're using this event as it's
+ // emitted very early in the document lifecycle (i.e. before any script on the page is
+ // executed), which is not the case for "window-global-created" for example.
+ Services.obs.addObserver(
+ this._onBrowsingContextAttached,
+ "browsing-context-attached"
+ );
+
+ // When we perform a bfcache navigation, the current browsing context gets
+ // replaced with a browsing which was previously stored in bfcache and we
+ // should update our reference accordingly.
+ this._onBfCacheNavigation = this._onBfCacheNavigation.bind(this);
+ this.watcherActor.on(
+ "bf-cache-navigation-pageshow",
+ this._onBfCacheNavigation
+ );
+
+ this._browsingContext = this.watcherActor.browserElement?.browsingContext;
+ }
+
+ form() {
+ return {
+ actor: this.actorID,
+ configuration: this._getConfiguration(),
+ traits: { supportedOptions: SUPPORTED_OPTIONS },
+ };
+ }
+
+ /**
+ * Returns whether or not this actor should handle the flag that should be set on the
+ * BrowsingContext in the parent process.
+ *
+ * @returns {Boolean}
+ */
+ _shouldHandleConfigurationInParentProcess() {
+ // Only handle parent process configuration if the watcherActor is tied to a
+ // browser element.
+ // For now, the Browser Toolbox and Web Extension are having a unique target
+ // which applies the configuration by itself on new documents.
+ return this.watcherActor.sessionContext.type == "browser-element";
+ }
+
+ /**
+ * Event handler for attached browsing context. This will be called when
+ * a new browsing context is created that we might want to handle
+ * (e.g. when navigating to a page with Cross-Origin-Opener-Policy header)
+ */
+ _onBrowsingContextAttached(browsingContext) {
+ if (!this._shouldHandleConfigurationInParentProcess()) {
+ return;
+ }
+
+ // We only want to set flags on top-level browsing context. The platform
+ // will take care of propagating it to the entire browsing contexts tree.
+ if (browsingContext.parent) {
+ return;
+ }
+
+ // Only process BrowsingContexts which are related to the debugged scope.
+ // As this callback fires very early, the BrowsingContext may not have
+ // any WindowGlobal yet and so we ignore all checks dones against the WindowGlobal
+ // if there is none. Meaning we might accept more BrowsingContext than expected.
+ if (
+ !isBrowsingContextPartOfContext(
+ browsingContext,
+ this.watcherActor.sessionContext,
+ { acceptNoWindowGlobal: true, forceAcceptTopLevelTarget: true }
+ )
+ ) {
+ return;
+ }
+
+ const rdmEnabledInPreviousBrowsingContext = this._browsingContext.inRDMPane;
+
+ // Before replacing the target browsing context, restore the configuration
+ // on the previous one if they share the same browser.
+ if (
+ this._browsingContext &&
+ this._browsingContext.browserId === browsingContext.browserId &&
+ !this._browsingContext.isDiscarded
+ ) {
+ // For now this should always be true as long as we already had a browsing
+ // context set, but the same logic should be used when supporting EFT on
+ // toolboxes with several top level browsing contexts: when a new browsing
+ // context attaches, only reset the browsing context with the same browserId
+ this._restoreParentProcessConfiguration();
+ }
+
+ // We need to store the browsing context as this.watcherActor.browserElement.browsingContext
+ // can still refer to the previous browsing context at this point.
+ this._browsingContext = browsingContext;
+
+ // If `inRDMPane` was set in the previous browsing context, set it again on the new one,
+ // otherwise some RDM-related configuration won't be applied (e.g. orientation).
+ if (rdmEnabledInPreviousBrowsingContext) {
+ this._browsingContext.inRDMPane = true;
+ }
+ this._updateParentProcessConfiguration(this._getConfiguration());
+ }
+
+ _onBfCacheNavigation({ windowGlobal } = {}) {
+ if (windowGlobal) {
+ this._onBrowsingContextAttached(windowGlobal.browsingContext);
+ }
+ }
+
+ _getConfiguration() {
+ const targetConfigurationData =
+ this.watcherActor.getSessionDataForType(TARGET_CONFIGURATION);
+ if (!targetConfigurationData) {
+ return {};
+ }
+
+ const cfgMap = {};
+ for (const { key, value } of targetConfigurationData) {
+ cfgMap[key] = value;
+ }
+ return cfgMap;
+ }
+
+ /**
+ *
+ * @param {Object} configuration
+ * @returns Promise<Object> Applied configuration object
+ */
+ async updateConfiguration(configuration) {
+ const cfgArray = Object.keys(configuration)
+ .filter(key => {
+ if (!SUPPORTED_OPTIONS[key]) {
+ console.warn(`Unsupported option for TargetConfiguration: ${key}`);
+ return false;
+ }
+ return true;
+ })
+ .map(key => ({ key, value: configuration[key] }));
+
+ this._updateParentProcessConfiguration(configuration);
+ await this.watcherActor.addOrSetDataEntry(
+ TARGET_CONFIGURATION,
+ cfgArray,
+ "add"
+ );
+ return this._getConfiguration();
+ }
+
+ /**
+ *
+ * @param {Object} configuration: See `updateConfiguration`
+ */
+ _updateParentProcessConfiguration(configuration) {
+ if (!this._shouldHandleConfigurationInParentProcess()) {
+ return;
+ }
+
+ let shouldReload = false;
+ 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) {
+ // This flag requires a reload in order to take full effect,
+ // so reload if it has changed.
+ if (value != this.isJavascriptEnabled()) {
+ shouldReload = true;
+ }
+ this._setJavascriptEnabled(value);
+ }
+ break;
+ case "overrideDPPX":
+ this._setDPPXOverride(value);
+ break;
+ case "printSimulationEnabled":
+ this._setPrintSimulationEnabled(value);
+ break;
+ case "rdmPaneMaxTouchPoints":
+ this._setRDMPaneMaxTouchPoints(value);
+ break;
+ case "rdmPaneOrientation":
+ this._setRDMPaneOrientation(value);
+ break;
+ case "serviceWorkersTestingEnabled":
+ this._setServiceWorkersTestingEnabled(value);
+ break;
+ case "touchEventsOverride":
+ this._setTouchEventsOverride(value);
+ break;
+ case "cacheDisabled":
+ this._setCacheDisabled(value);
+ break;
+ case "setTabOffline":
+ this._setTabOffline(value);
+ break;
+ }
+ }
+
+ if (shouldReload) {
+ this._browsingContext.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
+ }
+ }
+
+ _restoreParentProcessConfiguration() {
+ if (!this._shouldHandleConfigurationInParentProcess()) {
+ return;
+ }
+
+ this._setServiceWorkersTestingEnabled(false);
+ this._setPrintSimulationEnabled(false);
+ this._setCacheDisabled(false);
+ this._setTabOffline(false);
+
+ // Restore the color scheme simulation only if it was explicitly updated
+ // by this actor. This will avoid side effects caused when destroying additional
+ // targets (e.g. RDM target, WebExtension target, …).
+ // TODO: We may want to review other configuration values to see if we should use
+ // the same pattern (Bug 1701553).
+ if (this._resetColorSchemeSimulationOnDestroy) {
+ this._setColorSchemeSimulation(null);
+ }
+
+ // Restore the user agent only if it was explicitly updated by this specific actor.
+ if (this._initialUserAgent !== undefined) {
+ this._setCustomUserAgent(this._initialUserAgent);
+ }
+
+ // 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.
+ */
+ _setServiceWorkersTestingEnabled(enabled) {
+ if (this._browsingContext.serviceWorkersTestingEnabled != enabled) {
+ this._browsingContext.serviceWorkersTestingEnabled = enabled;
+ }
+ }
+
+ /**
+ * Disable or enable the print simulation.
+ */
+ _setPrintSimulationEnabled(enabled) {
+ const value = enabled ? "print" : "";
+ if (this._browsingContext.mediumOverride != value) {
+ this._browsingContext.mediumOverride = value;
+ }
+ }
+
+ /**
+ * Disable or enable the color-scheme simulation.
+ */
+ _setColorSchemeSimulation(override) {
+ const value = override || "none";
+ if (this._browsingContext.prefersColorSchemeOverride != value) {
+ this._browsingContext.prefersColorSchemeOverride = value;
+ this._resetColorSchemeSimulationOnDestroy = true;
+ }
+ }
+
+ /**
+ * Set a custom user agent on the page
+ *
+ * @param {String} userAgent: The user agent to set on the page. If null, will reset the
+ * user agent to its original value.
+ * @returns {Boolean} Whether the user agent was changed or not.
+ */
+ _setCustomUserAgent(userAgent = "") {
+ if (this._browsingContext.customUserAgent === userAgent) {
+ return;
+ }
+
+ 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;
+ } else if (dppx !== undefined && this._initialDPPXOverride === undefined) {
+ this._initialDPPXOverride = this._browsingContext.overrideDPPX;
+ }
+
+ if (dppx !== undefined) {
+ this._browsingContext.overrideDPPX = dppx;
+ }
+ }
+
+ /**
+ * Set the touchEventsOverride on the browsing context.
+ *
+ * @param {String} flag: See BrowsingContext.webidl `TouchEventsOverride` enum for values.
+ */
+ _setTouchEventsOverride(flag) {
+ if (this._browsingContext.touchEventsOverride === flag) {
+ return;
+ }
+
+ if (!flag && this._initialTouchEventsOverride) {
+ flag = this._initialTouchEventsOverride;
+ } else if (
+ flag !== undefined &&
+ this._initialTouchEventsOverride === undefined
+ ) {
+ this._initialTouchEventsOverride =
+ this._browsingContext.touchEventsOverride;
+ }
+
+ if (flag !== undefined) {
+ this._browsingContext.touchEventsOverride = flag;
+ }
+ }
+
+ /**
+ * Overrides navigator.maxTouchPoints.
+ * Note that we don't need to reset the original value when the actor is destroyed,
+ * as it's directly handled by the platform when RDM is closed.
+ *
+ * @param {Integer} maxTouchPoints
+ */
+ _setRDMPaneMaxTouchPoints(maxTouchPoints) {
+ this._browsingContext.setRDMPaneMaxTouchPoints(maxTouchPoints);
+ }
+
+ /**
+ * Set an orientation and an angle on the browsing context. This will be applied only
+ * if Responsive Design Mode is enabled.
+ *
+ * @param {Object} options
+ * @param {String} options.type: The orientation type of the rotated device.
+ * @param {Number} options.angle: The rotated angle of the device.
+ */
+ _setRDMPaneOrientation({ type, angle }) {
+ this._browsingContext.setRDMPaneOrientation(type, angle);
+ }
+
+ /**
+ * Disable or enable the cache via the browsing context.
+ *
+ * @param {Boolean} disabled: The state the cache should be changed to
+ */
+ _setCacheDisabled(disabled) {
+ const value = disabled
+ ? Ci.nsIRequest.LOAD_BYPASS_CACHE
+ : Ci.nsIRequest.LOAD_NORMAL;
+ if (this._browsingContext.defaultLoadFlags != value) {
+ this._browsingContext.defaultLoadFlags = value;
+ }
+ }
+
+ /**
+ * Set the browsing context to offline.
+ *
+ * @param {Boolean} offline: Whether the network throttling is set to offline
+ */
+ _setTabOffline(offline) {
+ if (!this._browsingContext.isDiscarded) {
+ this._browsingContext.forceOffline = offline;
+ }
+ }
+
+ destroy() {
+ Services.obs.removeObserver(
+ this._onBrowsingContextAttached,
+ "browsing-context-attached"
+ );
+ this.watcherActor.off(
+ "bf-cache-navigation-pageshow",
+ this._onBfCacheNavigation
+ );
+ this._restoreParentProcessConfiguration();
+ super.destroy();
+ }
+}
+
+exports.TargetConfigurationActor = TargetConfigurationActor;