diff options
Diffstat (limited to 'devtools/server/actors/target-configuration.js')
-rw-r--r-- | devtools/server/actors/target-configuration.js | 493 |
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; |