tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

target-configuration.js (19540B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const { Actor } = require("resource://devtools/shared/protocol.js");
      8 const {
      9  targetConfigurationSpec,
     10 } = require("resource://devtools/shared/specs/target-configuration.js");
     11 
     12 const { SessionDataHelpers } = ChromeUtils.importESModule(
     13  "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs",
     14  { global: "contextual" }
     15 );
     16 const { isBrowsingContextPartOfContext } = ChromeUtils.importESModule(
     17  "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
     18  { global: "contextual" }
     19 );
     20 loader.lazyRequireGetter(
     21  this,
     22  "TRACER_LOG_METHODS",
     23  "resource://devtools/shared/specs/tracer.js",
     24  true
     25 );
     26 const { SUPPORTED_DATA } = SessionDataHelpers;
     27 const { TARGET_CONFIGURATION } = SUPPORTED_DATA;
     28 const LOG_DISABLED = -1;
     29 
     30 // List of options supported by this target configuration actor.
     31 /* eslint sort-keys: "error" */
     32 const SUPPORTED_OPTIONS = {
     33  // Disable network request caching.
     34  cacheDisabled: true,
     35  // Enable color scheme simulation.
     36  colorSchemeSimulation: true,
     37  // Enable custom formatters
     38  customFormatters: true,
     39  // Set a custom user agent
     40  customUserAgent: true,
     41  // Is the tracer experimental feature manually enabled by the user?
     42  isTracerFeatureEnabled: true,
     43  // Enable JavaScript
     44  javascriptEnabled: true,
     45  // Force a custom device pixel ratio (used in RDM). Set to null to restore origin ratio.
     46  overrideDPPX: true,
     47  // Enable print simulation mode.
     48  printSimulationEnabled: true,
     49  // Override navigator.maxTouchPoints (used in RDM and doesn't apply if RDM isn't enabled)
     50  rdmPaneMaxTouchPoints: true,
     51  // Page orientation (used in RDM and doesn't apply if RDM isn't enabled)
     52  rdmPaneOrientation: true,
     53  // Enable allocation tracking, if set, contains an object defining the tracking configurations
     54  recordAllocations: true,
     55  // Reload the page when the touch simulation state changes (only works alongside touchEventsOverride)
     56  reloadOnTouchSimulationToggle: true,
     57  // Restore focus in the page after closing DevTools.
     58  restoreFocus: true,
     59  // Enable service worker testing over HTTP (instead of HTTPS only).
     60  serviceWorkersTestingEnabled: true,
     61  // Set the current tab offline
     62  setTabOffline: true,
     63  // Enable touch events simulation
     64  touchEventsOverride: true,
     65  // Used to configure and start/stop the JavaScript tracer
     66  tracerOptions: true,
     67  // Use simplified highlighters when prefers-reduced-motion is enabled.
     68  useSimpleHighlightersForReducedMotion: true,
     69 };
     70 /* eslint-disable sort-keys */
     71 
     72 /**
     73 * This actor manages the configuration flags which apply to DevTools targets.
     74 *
     75 * Configuration flags should be applied to all concerned targets when the
     76 * configuration is updated, and new targets should also be able to read the
     77 * flags when they are created. The flags will be forwarded to the WatcherActor
     78 * and stored as TARGET_CONFIGURATION data entries.
     79 * Some flags will be set directly set from this actor, in the parent process
     80 * (see _updateParentProcessConfiguration), and others will be set from the target actor,
     81 * in the content process.
     82 *
     83 * @class
     84 */
     85 class TargetConfigurationActor extends Actor {
     86  constructor(watcherActor) {
     87    super(watcherActor.conn, targetConfigurationSpec);
     88    this.watcherActor = watcherActor;
     89 
     90    this._onBrowsingContextAttached =
     91      this._onBrowsingContextAttached.bind(this);
     92    // We need to be notified of new browsing context being created so we can re-set flags
     93    // we already set on the "previous" browsing context. We're using this event  as it's
     94    // emitted very early in the document lifecycle (i.e. before any script on the page is
     95    // executed), which is not the case for "window-global-created" for example.
     96    Services.obs.addObserver(
     97      this._onBrowsingContextAttached,
     98      "browsing-context-attached"
     99    );
    100 
    101    // When we perform a bfcache navigation, the current browsing context gets
    102    // replaced with a browsing which was previously stored in bfcache and we
    103    // should update our reference accordingly.
    104    this._onBfCacheNavigation = this._onBfCacheNavigation.bind(this);
    105    this.watcherActor.on(
    106      "bf-cache-navigation-pageshow",
    107      this._onBfCacheNavigation
    108    );
    109 
    110    this._browsingContext = this.watcherActor.browserElement?.browsingContext;
    111  }
    112 
    113  // Value of `logging.console` pref, before starting recording JS Traces
    114  #consolePrefValue;
    115  // Value of `logging.PageMessages` pref, before starting recording JS Traces
    116  #pageMessagesPrefValue;
    117 
    118  form() {
    119    return {
    120      actor: this.actorID,
    121      configuration: this._getConfiguration(),
    122      traits: { supportedOptions: SUPPORTED_OPTIONS },
    123    };
    124  }
    125 
    126  /**
    127   * Returns whether or not this actor should handle the flag that should be set on the
    128   * BrowsingContext in the parent process.
    129   *
    130   * @returns {boolean}
    131   */
    132  _shouldHandleConfigurationInParentProcess() {
    133    // Only handle parent process configuration if the watcherActor is tied to a
    134    // browser element.
    135    // For now, the Browser Toolbox and Web Extension are having a unique target
    136    // which applies the configuration by itself on new documents.
    137    return this.watcherActor.sessionContext.type == "browser-element";
    138  }
    139 
    140  /**
    141   * Event handler for attached browsing context. This will be called when
    142   * a new browsing context is created that we might want to handle
    143   * (e.g. when navigating to a page with Cross-Origin-Opener-Policy header)
    144   */
    145  _onBrowsingContextAttached(browsingContext) {
    146    if (!this._shouldHandleConfigurationInParentProcess()) {
    147      return;
    148    }
    149 
    150    // We only want to set flags on top-level browsing context. The platform
    151    // will take care of propagating it to the entire browsing contexts tree.
    152    if (browsingContext.parent) {
    153      return;
    154    }
    155 
    156    // Only process BrowsingContexts which are related to the debugged scope.
    157    // As this callback fires very early, the BrowsingContext may not have
    158    // any WindowGlobal yet and so we ignore all checks dones against the WindowGlobal
    159    // if there is none. Meaning we might accept more BrowsingContext than expected.
    160    if (
    161      !isBrowsingContextPartOfContext(
    162        browsingContext,
    163        this.watcherActor.sessionContext,
    164        { acceptNoWindowGlobal: true, forceAcceptTopLevelTarget: true }
    165      )
    166    ) {
    167      return;
    168    }
    169 
    170    const rdmEnabledInPreviousBrowsingContext = this._browsingContext.inRDMPane;
    171 
    172    // Before replacing the target browsing context, restore the configuration
    173    // on the previous one if they share the same browser.
    174    if (
    175      this._browsingContext &&
    176      this._browsingContext.browserId === browsingContext.browserId &&
    177      !this._browsingContext.isDiscarded
    178    ) {
    179      // For now this should always be true as long as we already had a browsing
    180      // context set, but the same logic should be used when supporting EFT on
    181      // toolboxes with several top level browsing contexts: when a new browsing
    182      // context attaches, only reset the browsing context with the same browserId
    183      this._restoreParentProcessConfiguration();
    184    }
    185 
    186    // We need to store the browsing context as this.watcherActor.browserElement.browsingContext
    187    // can still refer to the previous browsing context at this point.
    188    this._browsingContext = browsingContext;
    189 
    190    // If `inRDMPane` was set in the previous browsing context, set it again on the new one,
    191    // otherwise some RDM-related configuration won't be applied (e.g. orientation).
    192    if (rdmEnabledInPreviousBrowsingContext) {
    193      this._browsingContext.inRDMPane = true;
    194    }
    195    this._updateParentProcessConfiguration(this._getConfiguration());
    196  }
    197 
    198  _onBfCacheNavigation({ windowGlobal } = {}) {
    199    if (windowGlobal) {
    200      this._onBrowsingContextAttached(windowGlobal.browsingContext);
    201    }
    202  }
    203 
    204  _getConfiguration() {
    205    const targetConfigurationData =
    206      this.watcherActor.getSessionDataForType(TARGET_CONFIGURATION);
    207    if (!targetConfigurationData) {
    208      return {};
    209    }
    210 
    211    const cfgMap = {};
    212    for (const { key, value } of targetConfigurationData) {
    213      cfgMap[key] = value;
    214    }
    215    return cfgMap;
    216  }
    217 
    218  /**
    219   *
    220   * @param {object} configuration
    221   * @returns Promise<Object> Applied configuration object
    222   */
    223  async updateConfiguration(configuration) {
    224    const cfgArray = Object.keys(configuration)
    225      .filter(key => {
    226        if (!SUPPORTED_OPTIONS[key]) {
    227          console.warn(`Unsupported option for TargetConfiguration: ${key}`);
    228          return false;
    229        }
    230        return true;
    231      })
    232      .map(key => ({ key, value: configuration[key] }));
    233 
    234    this._updateParentProcessConfiguration(configuration);
    235    await this.watcherActor.addOrSetDataEntry(
    236      TARGET_CONFIGURATION,
    237      cfgArray,
    238      "add"
    239    );
    240    return this._getConfiguration();
    241  }
    242 
    243  /**
    244   *
    245   * @param {object} configuration: See `updateConfiguration`
    246   */
    247  _updateParentProcessConfiguration(configuration) {
    248    // Process "tracerOptions" for all session types, as this isn't specific to tab debugging
    249    if ("tracerOptions" in configuration) {
    250      this._setTracerOptions(configuration.tracerOptions);
    251    }
    252 
    253    if (!this._shouldHandleConfigurationInParentProcess()) {
    254      return;
    255    }
    256 
    257    let shouldReload = false;
    258    for (const [key, value] of Object.entries(configuration)) {
    259      switch (key) {
    260        case "colorSchemeSimulation":
    261          this._setColorSchemeSimulation(value);
    262          break;
    263        case "customUserAgent":
    264          this._setCustomUserAgent(value);
    265          break;
    266        case "javascriptEnabled":
    267          if (value !== undefined) {
    268            // This flag requires a reload in order to take full effect,
    269            // so reload if it has changed.
    270            if (value != this.isJavascriptEnabled()) {
    271              shouldReload = true;
    272            }
    273            this._setJavascriptEnabled(value);
    274          }
    275          break;
    276        case "overrideDPPX":
    277          this._setDPPXOverride(value);
    278          break;
    279        case "printSimulationEnabled":
    280          this._setPrintSimulationEnabled(value);
    281          break;
    282        case "rdmPaneMaxTouchPoints":
    283          this._setRDMPaneMaxTouchPoints(value);
    284          break;
    285        case "rdmPaneOrientation":
    286          this._setRDMPaneOrientation(value);
    287          break;
    288        case "serviceWorkersTestingEnabled":
    289          this._setServiceWorkersTestingEnabled(value);
    290          break;
    291        case "touchEventsOverride":
    292          this._setTouchEventsOverride(value);
    293          break;
    294        case "cacheDisabled":
    295          this._setCacheDisabled(value);
    296          break;
    297        case "setTabOffline":
    298          this._setTabOffline(value);
    299          break;
    300      }
    301    }
    302 
    303    if (shouldReload) {
    304      this._browsingContext.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
    305    }
    306  }
    307 
    308  _restoreParentProcessConfiguration() {
    309    // Always process tracer options as this isn't specific to tab debugging
    310    if (this.#consolePrefValue !== undefined) {
    311      this._setTracerOptions();
    312    }
    313 
    314    if (!this._shouldHandleConfigurationInParentProcess()) {
    315      return;
    316    }
    317 
    318    this._setServiceWorkersTestingEnabled(false);
    319    this._setPrintSimulationEnabled(false);
    320    if (this._resetCacheDisabledOnDestroy) {
    321      this._setCacheDisabled(false);
    322    }
    323    this._setTabOffline(false);
    324 
    325    // Restore the color scheme simulation only if it was explicitly updated
    326    // by this actor. This will avoid side effects caused when destroying additional
    327    // targets (e.g. RDM target, WebExtension target, …).
    328    // TODO: We may want to review other configuration values to see if we should use
    329    // the same pattern (Bug 1701553).
    330    if (this._resetColorSchemeSimulationOnDestroy) {
    331      this._setColorSchemeSimulation(null);
    332    }
    333 
    334    // Restore the user agent only if it was explicitly updated by this specific actor.
    335    if (this._initialUserAgent !== undefined) {
    336      this._setCustomUserAgent(this._initialUserAgent);
    337    }
    338 
    339    // Restore the origin device pixel ratio only if it was explicitly updated by this
    340    // specific actor.
    341    if (this._initialDPPXOverride !== undefined) {
    342      this._setDPPXOverride(this._initialDPPXOverride);
    343    }
    344 
    345    if (this._initialJavascriptEnabled !== undefined) {
    346      this._setJavascriptEnabled(this._initialJavascriptEnabled);
    347    }
    348 
    349    if (this._initialTouchEventsOverride !== undefined) {
    350      this._setTouchEventsOverride(this._initialTouchEventsOverride);
    351    }
    352  }
    353 
    354  /**
    355   * Disable or enable the service workers testing features.
    356   */
    357  _setServiceWorkersTestingEnabled(enabled) {
    358    if (this._browsingContext.serviceWorkersTestingEnabled != enabled) {
    359      this._browsingContext.serviceWorkersTestingEnabled = enabled;
    360    }
    361  }
    362 
    363  /**
    364   * Disable or enable the print simulation.
    365   */
    366  _setPrintSimulationEnabled(enabled) {
    367    const value = enabled ? "print" : "";
    368    if (this._browsingContext.mediumOverride != value) {
    369      this._browsingContext.mediumOverride = value;
    370    }
    371  }
    372 
    373  /**
    374   * Disable or enable the color-scheme simulation.
    375   */
    376  _setColorSchemeSimulation(override) {
    377    const value = override || "none";
    378    if (this._browsingContext.prefersColorSchemeOverride != value) {
    379      this._browsingContext.prefersColorSchemeOverride = value;
    380      this._resetColorSchemeSimulationOnDestroy = true;
    381    }
    382  }
    383 
    384  /**
    385   * Set a custom user agent on the page
    386   *
    387   * @param {string} userAgent: The user agent to set on the page. If null, will reset the
    388   *                 user agent to its original value.
    389   * @returns {boolean} Whether the user agent was changed or not.
    390   */
    391  _setCustomUserAgent(userAgent = "") {
    392    if (this._browsingContext.customUserAgent === userAgent) {
    393      return;
    394    }
    395 
    396    if (this._initialUserAgent === undefined) {
    397      this._initialUserAgent = this._browsingContext.customUserAgent;
    398    }
    399 
    400    this._browsingContext.customUserAgent = userAgent;
    401  }
    402 
    403  isJavascriptEnabled() {
    404    return this._browsingContext.allowJavascript;
    405  }
    406 
    407  _setJavascriptEnabled(allow) {
    408    if (this._initialJavascriptEnabled === undefined) {
    409      this._initialJavascriptEnabled = this._browsingContext.allowJavascript;
    410    }
    411    if (allow !== undefined) {
    412      this._browsingContext.allowJavascript = allow;
    413    }
    414  }
    415 
    416  /* DPPX override */
    417  _setDPPXOverride(dppx) {
    418    if (this._browsingContext.overrideDPPX === dppx) {
    419      return;
    420    }
    421 
    422    if (!dppx && this._initialDPPXOverride) {
    423      dppx = this._initialDPPXOverride;
    424    } else if (dppx !== undefined && this._initialDPPXOverride === undefined) {
    425      this._initialDPPXOverride = this._browsingContext.overrideDPPX;
    426    }
    427 
    428    if (dppx !== undefined) {
    429      this._browsingContext.overrideDPPX = dppx;
    430    }
    431  }
    432 
    433  /**
    434   * Set the touchEventsOverride on the browsing context.
    435   *
    436   * @param {string} flag: See BrowsingContext.webidl `TouchEventsOverride` enum for values.
    437   */
    438  _setTouchEventsOverride(flag) {
    439    if (this._browsingContext.touchEventsOverride === flag) {
    440      return;
    441    }
    442 
    443    if (!flag && this._initialTouchEventsOverride) {
    444      flag = this._initialTouchEventsOverride;
    445    } else if (
    446      flag !== undefined &&
    447      this._initialTouchEventsOverride === undefined
    448    ) {
    449      this._initialTouchEventsOverride =
    450        this._browsingContext.touchEventsOverride;
    451    }
    452 
    453    if (flag !== undefined) {
    454      this._browsingContext.touchEventsOverride = flag;
    455    }
    456  }
    457 
    458  /**
    459   * Overrides navigator.maxTouchPoints.
    460   * Note that we don't need to reset the original value when the actor is destroyed,
    461   * as it's directly handled by the platform when RDM is closed.
    462   *
    463   * @param {Integer} maxTouchPoints
    464   */
    465  _setRDMPaneMaxTouchPoints(maxTouchPoints) {
    466    this._browsingContext.setRDMPaneMaxTouchPoints(maxTouchPoints);
    467  }
    468 
    469  /**
    470   * Set an orientation and an angle on the browsing context. This will be applied only
    471   * if Responsive Design Mode is enabled.
    472   *
    473   * @param {object} options
    474   * @param {string} options.type: The orientation type of the rotated device.
    475   * @param {number} options.angle: The rotated angle of the device.
    476   */
    477  _setRDMPaneOrientation({ type, angle }) {
    478    if (this._browsingContext.inRDMPane) {
    479      this._browsingContext.setOrientationOverride(type, angle);
    480    }
    481  }
    482 
    483  /**
    484   * Disable or enable the cache via the browsing context.
    485   *
    486   * @param {boolean} disabled: The state the cache should be changed to
    487   */
    488  _setCacheDisabled(disabled) {
    489    const value = disabled
    490      ? Ci.nsIRequest.LOAD_BYPASS_CACHE
    491      : Ci.nsIRequest.LOAD_NORMAL;
    492    if (this._browsingContext.defaultLoadFlags != value) {
    493      this._browsingContext.defaultLoadFlags = value;
    494      this._resetCacheDisabledOnDestroy = true;
    495    }
    496  }
    497 
    498  /**
    499   * Set the browsing context to offline.
    500   *
    501   * @param {boolean} offline: Whether the network throttling is set to offline
    502   */
    503  _setTabOffline(offline) {
    504    if (!this._browsingContext.isDiscarded) {
    505      this._browsingContext.forceOffline = offline;
    506    }
    507  }
    508 
    509  destroy() {
    510    Services.obs.removeObserver(
    511      this._onBrowsingContextAttached,
    512      "browsing-context-attached"
    513    );
    514    this.watcherActor.off(
    515      "bf-cache-navigation-pageshow",
    516      this._onBfCacheNavigation
    517    );
    518    // Avoid trying to restore if the related context is already being destroyed
    519    if (this._browsingContext && !this._browsingContext.isDiscarded) {
    520      this._restoreParentProcessConfiguration();
    521    }
    522    super.destroy();
    523  }
    524 
    525  /**
    526   * Called when the tracer is toggled on/off by the frontend.
    527   * Note that when `options` is defined, it is meant to be enabled.
    528   * It may not actually be tracing yet depending on the passed options.
    529   *
    530   * @param {object} options
    531   */
    532  _setTracerOptions(options) {
    533    if (!options) {
    534      if (this.#consolePrefValue === LOG_DISABLED) {
    535        Services.prefs.clearUserPref("logging.console");
    536      } else {
    537        Services.prefs.setIntPref("logging.console", this.#consolePrefValue);
    538      }
    539      this.#consolePrefValue = undefined;
    540      if (this.#pageMessagesPrefValue === LOG_DISABLED) {
    541        Services.prefs.clearUserPref("logging.PageMessages");
    542      } else {
    543        Services.prefs.setIntPref(
    544          "logging.PageMessages",
    545          this.#pageMessagesPrefValue
    546        );
    547      }
    548      this.#pageMessagesPrefValue = undefined;
    549      return;
    550    }
    551 
    552    // Only enable the MOZ_LOG's when recording to the profiler,
    553    // otherwise it would pollute firefox stdout unexpectedly.
    554    if (options.logMethod != TRACER_LOG_METHODS.PROFILER) {
    555      return;
    556    }
    557 
    558    // Enable `MOZ_LOG=console:5` via the logging.console so that all console API calls
    559    // are stored in the profiler when recording JS Traces via the profiler.
    560    //
    561    // We do this from here as TargetConfiguration runs in the parent process,
    562    // where we can set preferences. Whereas the profiler tracer actor runs in the content process.
    563    const LOG_VERBOSE = 5;
    564    this.#consolePrefValue = Services.prefs.getIntPref(
    565      "logging.console",
    566      LOG_DISABLED
    567    );
    568    Services.prefs.setIntPref("logging.console", LOG_VERBOSE);
    569    this.#pageMessagesPrefValue = Services.prefs.getIntPref(
    570      "logging.PageMessages",
    571      LOG_DISABLED
    572    );
    573    Services.prefs.setIntPref("logging.PageMessages", LOG_VERBOSE);
    574  }
    575 }
    576 
    577 exports.TargetConfigurationActor = TargetConfigurationActor;