tor-browser

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

webextension.js (10469B)


      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 /*
      8 * Represents a WebExtension add-on in the parent process. This gives some metadata about
      9 * the add-on and watches for uninstall events. This uses a proxy to access the
     10 * WebExtension in the WebExtension process via the message manager.
     11 *
     12 * See devtools/docs/backend/actor-hierarchy.md for more details.
     13 */
     14 
     15 const { Actor } = require("resource://devtools/shared/protocol.js");
     16 const {
     17  webExtensionDescriptorSpec,
     18 } = require("resource://devtools/shared/specs/descriptors/webextension.js");
     19 
     20 const {
     21  createWebExtensionSessionContext,
     22 } = require("resource://devtools/server/actors/watcher/session-context.js");
     23 
     24 const lazy = {};
     25 loader.lazyGetter(lazy, "AddonManager", () => {
     26  return ChromeUtils.importESModule(
     27    "resource://gre/modules/AddonManager.sys.mjs",
     28    { global: "shared" }
     29  ).AddonManager;
     30 });
     31 loader.lazyGetter(lazy, "ExtensionParent", () => {
     32  return ChromeUtils.importESModule(
     33    "resource://gre/modules/ExtensionParent.sys.mjs",
     34    { global: "shared" }
     35  ).ExtensionParent;
     36 });
     37 loader.lazyRequireGetter(
     38  this,
     39  "WatcherActor",
     40  "resource://devtools/server/actors/watcher.js",
     41  true
     42 );
     43 
     44 const { WEBEXTENSION_FALLBACK_DOC_URL } = ChromeUtils.importESModule(
     45  "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
     46  { global: "contextual" }
     47 );
     48 
     49 const BGSCRIPT_STATUSES = {
     50  RUNNING: "RUNNING",
     51  STOPPED: "STOPPED",
     52 };
     53 
     54 /**
     55 * Creates the actor that represents the addon in the parent process, which relies
     56 * on its child Watcher Actor to expose all WindowGlobal target actors for all
     57 * the active documents involved in the debugged addon.
     58 *
     59 * The WebExtensionDescriptorActor subscribes itself as an AddonListener on the AddonManager
     60 * and forwards this events to child actor (e.g. on addon reload or when the addon is
     61 * uninstalled completely) and connects to the child extension process using a `browser`
     62 * element provided by the extension internals (it is not related to any single extension,
     63 * but it will be created automatically to the currently selected "WebExtensions OOP mode"
     64 * and it persist across the extension reloads.
     65 *
     66 * The descriptor will also be persisted when the target actor is destroyed, so
     67 * that we can reuse the same descriptor for several remote debugging toolboxes
     68 * from about:debugging.
     69 *
     70 * WebExtensionDescriptorActor is a child of RootActor, it can be retrieved via
     71 * RootActor.listAddons request.
     72 *
     73 * @param {DevToolsServerConnection} conn
     74 *        The connection to the client.
     75 * @param {AddonWrapper} addon
     76 *        The target addon.
     77 */
     78 class WebExtensionDescriptorActor extends Actor {
     79  constructor(conn, addon) {
     80    super(conn, webExtensionDescriptorSpec);
     81    this.addon = addon;
     82    this.addonId = addon.id;
     83 
     84    this.destroy = this.destroy.bind(this);
     85 
     86    lazy.AddonManager.addAddonListener(this);
     87  }
     88 
     89  form() {
     90    const { addonId } = this;
     91    const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(addonId);
     92    const persistentBackgroundScript =
     93      lazy.ExtensionParent.DebugUtils.hasPersistentBackgroundScript(addonId);
     94    const backgroundScriptStatus = this._getBackgroundScriptStatus();
     95 
     96    return {
     97      actor: this.actorID,
     98      backgroundScriptStatus,
     99      // Note that until the policy becomes active,
    100      // getWatcher will fail attaching to the web extension:
    101      // https://searchfox.org/mozilla-central/rev/526a5089c61db85d4d43eb0e46edaf1f632e853a/toolkit/components/extensions/WebExtensionPolicy.cpp#551-553
    102      debuggable: policy?.active && this.addon.isDebuggable,
    103      hidden: this.addon.hidden,
    104      // iconDataURL is available after calling loadIconDataURL
    105      iconDataURL: this._iconDataURL,
    106      iconURL: this.addon.iconURL,
    107      id: addonId,
    108      isSystem: this.addon.isSystem,
    109      isWebExtension: this.addon.isWebExtension,
    110      manifestURL: policy && policy.getURL("manifest.json"),
    111      name: this.addon.name,
    112      persistentBackgroundScript,
    113      temporarilyInstalled: this.addon.temporarilyInstalled,
    114      traits: {
    115        supportsReloadDescriptor: true,
    116        // Supports the Watcher actor. Can be removed as part of Bug 1680280.
    117        watcher: true,
    118      },
    119      url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
    120      warnings: lazy.ExtensionParent.DebugUtils.getExtensionManifestWarnings(
    121        this.addonId
    122      ),
    123    };
    124  }
    125 
    126  /**
    127   * Return a Watcher actor, allowing to keep track of targets which
    128   * already exists or will be created. It also helps knowing when they
    129   * are destroyed.
    130   */
    131  async getWatcher(config = {}) {
    132    if (!this.watcher) {
    133      // Spawn an empty document so that we always have an active WindowGlobal,
    134      // so that we can always instantiate a top level WindowGlobal target to the frontend.
    135      await this.#createFallbackDocument();
    136 
    137      this.watcher = new WatcherActor(
    138        this.conn,
    139        createWebExtensionSessionContext(
    140          {
    141            addonId: this.addonId,
    142          },
    143          config
    144        )
    145      );
    146      this.manage(this.watcher);
    147    }
    148    return this.watcher;
    149  }
    150 
    151  /**
    152   * Create an empty document to circumvant the lack of any WindowGlobal/document
    153   * running for this addon.
    154   *
    155   * For now DevTools always expect at least one Target to be functional,
    156   * and we need a document to spawn a target actor.
    157   */
    158  async #createFallbackDocument() {
    159    if (this._browser) {
    160      return;
    161    }
    162 
    163    // The extension process browser will only be released on descriptor destruction and can
    164    // be reused for subsequent watchers if we close and reopen a toolbox from about:debugging.
    165    //
    166    // Note that this `getExtensionProcessBrowser` will register the DevTools to the extension codebase.
    167    // If we stop creating a fallback document, we should register DevTools by some other means.
    168    this._browser =
    169      await lazy.ExtensionParent.DebugUtils.getExtensionProcessBrowser(this);
    170 
    171    // As "load" event isn't fired on the <browser> element, use a Web Progress Listener
    172    // in order to wait for the full loading of that fallback document.
    173    // It prevents having to deal with the initial about:blank document in the content processes.
    174    // We have various checks to identify the fallback document based on its URL.
    175    // It also ensure that the fallback document is created before the watcher starts
    176    // and helps spawning the target for that document first.
    177    const onLocationChanged = new Promise(resolve => {
    178      const listener = {
    179        onLocationChange: () => {
    180          this._browser.webProgress.removeProgressListener(listener);
    181          resolve();
    182        },
    183        QueryInterface: ChromeUtils.generateQI([
    184          "nsIWebProgressListener",
    185          "nsISupportsWeakReference",
    186        ]),
    187      };
    188 
    189      this._browser.webProgress.addProgressListener(
    190        listener,
    191        Ci.nsIWebProgress.NOTIFY_LOCATION
    192      );
    193    });
    194 
    195    // Add the addonId in the URL to retrieve this information in other devtools
    196    // helpers. The addonId is usually populated in the principal, but this will
    197    // not be the case for the fallback window because it is loaded from chrome://
    198    // instead of moz-extension://${addonId}
    199    this._browser.setAttribute(
    200      "src",
    201      `${WEBEXTENSION_FALLBACK_DOC_URL}#${this.addonId}`
    202    );
    203    await onLocationChanged;
    204  }
    205 
    206  /**
    207   * Note that reloadDescriptor is the common API name for descriptors
    208   * which support to be reloaded, while WebExtensionDescriptorActor::reload
    209   * is a legacy API which is for instance used from web-ext.
    210   *
    211   * bypassCache has no impact for addon reloads.
    212   */
    213  reloadDescriptor() {
    214    return this.reload();
    215  }
    216 
    217  async reload() {
    218    await this.addon.reload();
    219    return {};
    220  }
    221 
    222  async terminateBackgroundScript() {
    223    await lazy.ExtensionParent.DebugUtils.terminateBackgroundScript(
    224      this.addonId
    225    );
    226  }
    227 
    228  // This function will be called from RootActor in case that the devtools client
    229  // retrieves list of addons with `iconDataURL` option.
    230  async loadIconDataURL() {
    231    this._iconDataURL = await this.getIconDataURL();
    232  }
    233 
    234  async getIconDataURL() {
    235    if (!this.addon.iconURL) {
    236      return null;
    237    }
    238 
    239    const xhr = new XMLHttpRequest();
    240    xhr.responseType = "blob";
    241    xhr.open("GET", this.addon.iconURL, true);
    242 
    243    if (this.addon.iconURL.toLowerCase().endsWith(".svg")) {
    244      // Maybe SVG, thus force to change mime type.
    245      xhr.overrideMimeType("image/svg+xml");
    246    }
    247 
    248    try {
    249      const blob = await new Promise((resolve, reject) => {
    250        xhr.onload = () => resolve(xhr.response);
    251        xhr.onerror = reject;
    252        xhr.send();
    253      });
    254 
    255      const reader = new FileReader();
    256      return await new Promise((resolve, reject) => {
    257        reader.onloadend = () => resolve(reader.result);
    258        reader.onerror = reject;
    259        reader.readAsDataURL(blob);
    260      });
    261    } catch (_) {
    262      console.warn(`Failed to create data url from [${this.addon.iconURL}]`);
    263      return null;
    264    }
    265  }
    266 
    267  // Private Methods
    268  _getBackgroundScriptStatus() {
    269    const isRunning = lazy.ExtensionParent.DebugUtils.isBackgroundScriptRunning(
    270      this.addonId
    271    );
    272    // The background script status doesn't apply to this addon (e.g. the addon
    273    // type doesn't have any code, like staticthemes/langpacks/dictionaries, or
    274    // the extension does not have a background script at all).
    275    if (isRunning === undefined) {
    276      return undefined;
    277    }
    278 
    279    return isRunning ? BGSCRIPT_STATUSES.RUNNING : BGSCRIPT_STATUSES.STOPPED;
    280  }
    281 
    282  // AddonManagerListener callbacks.
    283  onInstalled(addon) {
    284    if (addon.id != this.addonId) {
    285      return;
    286    }
    287 
    288    // Update the AddonManager's addon object on reload/update.
    289    this.addon = addon;
    290  }
    291 
    292  onUninstalled(addon) {
    293    if (addon != this.addon) {
    294      return;
    295    }
    296 
    297    this.destroy();
    298  }
    299 
    300  destroy() {
    301    lazy.AddonManager.removeAddonListener(this);
    302 
    303    this.addon = null;
    304 
    305    if (this.watcher) {
    306      this.watcher = null;
    307    }
    308 
    309    if (this._browser) {
    310      lazy.ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this);
    311      this._browser = null;
    312    }
    313 
    314    this.emit("descriptor-destroyed");
    315 
    316    super.destroy();
    317  }
    318 }
    319 
    320 exports.WebExtensionDescriptorActor = WebExtensionDescriptorActor;