summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/descriptors/webextension.js
blob: 56e4abfc41926007cb4372981635978540feb2af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
/* 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";

/*
 * Represents a WebExtension add-on in the parent process. This gives some metadata about
 * the add-on and watches for uninstall events. This uses a proxy to access the
 * WebExtension in the WebExtension process via the message manager.
 *
 * See devtools/docs/backend/actor-hierarchy.md for more details.
 */

const { Actor } = require("resource://devtools/shared/protocol.js");
const {
  webExtensionDescriptorSpec,
} = require("resource://devtools/shared/specs/descriptors/webextension.js");

const {
  connectToFrame,
} = require("resource://devtools/server/connectors/frame-connector.js");
const {
  createWebExtensionSessionContext,
} = require("resource://devtools/server/actors/watcher/session-context.js");

const lazy = {};
loader.lazyGetter(lazy, "AddonManager", () => {
  return ChromeUtils.importESModule(
    "resource://gre/modules/AddonManager.sys.mjs",
    { loadInDevToolsLoader: false }
  ).AddonManager;
});
loader.lazyGetter(lazy, "ExtensionParent", () => {
  return ChromeUtils.importESModule(
    "resource://gre/modules/ExtensionParent.sys.mjs",
    { loadInDevToolsLoader: false }
  ).ExtensionParent;
});
loader.lazyRequireGetter(
  this,
  "WatcherActor",
  "resource://devtools/server/actors/watcher.js",
  true
);

const BGSCRIPT_STATUSES = {
  RUNNING: "RUNNING",
  STOPPED: "STOPPED",
};

/**
 * Creates the actor that represents the addon in the parent process, which connects
 * itself to a WebExtensionTargetActor counterpart which is created in the extension
 * process (or in the main process if the WebExtensions OOP mode is disabled).
 *
 * The WebExtensionDescriptorActor subscribes itself as an AddonListener on the AddonManager
 * and forwards this events to child actor (e.g. on addon reload or when the addon is
 * uninstalled completely) and connects to the child extension process using a `browser`
 * element provided by the extension internals (it is not related to any single extension,
 * but it will be created automatically to the currently selected "WebExtensions OOP mode"
 * and it persist across the extension reloads (it is destroyed once the actor exits).
 * WebExtensionDescriptorActor is a child of RootActor, it can be retrieved via
 * RootActor.listAddons request.
 *
 * @param {DevToolsServerConnection} conn
 *        The connection to the client.
 * @param {AddonWrapper} addon
 *        The target addon.
 */
class WebExtensionDescriptorActor extends Actor {
  constructor(conn, addon) {
    super(conn, webExtensionDescriptorSpec);
    this.addon = addon;
    this.addonId = addon.id;
    this._childFormPromise = null;

    this._onChildExit = this._onChildExit.bind(this);
    this.destroy = this.destroy.bind(this);
    lazy.AddonManager.addAddonListener(this);
  }

  form() {
    const { addonId } = this;
    const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(addonId);
    const persistentBackgroundScript =
      lazy.ExtensionParent.DebugUtils.hasPersistentBackgroundScript(addonId);
    const backgroundScriptStatus = this._getBackgroundScriptStatus();

    return {
      actor: this.actorID,
      backgroundScriptStatus,
      // Note that until the policy becomes active,
      // getTarget/connectToFrame will fail attaching to the web extension:
      // https://searchfox.org/mozilla-central/rev/526a5089c61db85d4d43eb0e46edaf1f632e853a/toolkit/components/extensions/WebExtensionPolicy.cpp#551-553
      debuggable: policy?.active && this.addon.isDebuggable,
      hidden: this.addon.hidden,
      // iconDataURL is available after calling loadIconDataURL
      iconDataURL: this._iconDataURL,
      iconURL: this.addon.iconURL,
      id: addonId,
      isSystem: this.addon.isSystem,
      isWebExtension: this.addon.isWebExtension,
      manifestURL: policy && policy.getURL("manifest.json"),
      name: this.addon.name,
      persistentBackgroundScript,
      temporarilyInstalled: this.addon.temporarilyInstalled,
      traits: {
        supportsReloadDescriptor: true,
        // Supports the Watcher actor. Can be removed as part of Bug 1680280.
        watcher: true,
      },
      url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
      warnings: lazy.ExtensionParent.DebugUtils.getExtensionManifestWarnings(
        this.addonId
      ),
    };
  }

  /**
   * Return a Watcher actor, allowing to keep track of targets which
   * already exists or will be created. It also helps knowing when they
   * are destroyed.
   */
  async getWatcher(config = {}) {
    if (!this.watcher) {
      // Ensure connecting to the webextension frame in order to populate this._form
      await this._extensionFrameConnect();
      this.watcher = new WatcherActor(
        this.conn,
        createWebExtensionSessionContext(
          {
            addonId: this.addonId,
            browsingContextID: this._form.browsingContextID,
            innerWindowId: this._form.innerWindowId,
          },
          config
        )
      );
      this.manage(this.watcher);
    }
    return this.watcher;
  }

  async getTarget() {
    const form = await this._extensionFrameConnect();
    // Merge into the child actor form, some addon metadata
    // (e.g. the addon name shown in the addon debugger window title).
    return Object.assign(form, {
      iconURL: this.addon.iconURL,
      id: this.addon.id,
      name: this.addon.name,
    });
  }

  getChildren() {
    return [];
  }

  async _extensionFrameConnect() {
    if (this._form) {
      return this._form;
    }

    this._browser =
      await lazy.ExtensionParent.DebugUtils.getExtensionProcessBrowser(this);

    const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(
      this.addonId
    );
    this._form = await connectToFrame(this.conn, this._browser, this.destroy, {
      addonId: this.addonId,
      addonBrowsingContextGroupId: policy.browsingContextGroupId,
      // Bug 1754452: This flag is passed by the client to getWatcher(), but the server
      // doesn't support this anyway. So always pass false here and keep things simple.
      // Once we enable this flag, we will stop using connectToFrame and instantiate
      // the WebExtensionTargetActor from watcher code instead, so that shouldn't
      // introduce an issue for the future.
      isServerTargetSwitchingEnabled: false,
    });

    // connectToFrame may resolve to a null form,
    // in case the browser element is destroyed before it is fully connected to it.
    if (!this._form) {
      throw new Error(
        "browser element destroyed while connecting to it: " + this.addon.name
      );
    }

    this._childActorID = this._form.actor;

    // Exit the proxy child actor if the child actor has been destroyed.
    this._mm.addMessageListener("debug:webext_child_exit", this._onChildExit);

    return this._form;
  }

  /**
   * Note that reloadDescriptor is the common API name for descriptors
   * which support to be reloaded, while WebExtensionDescriptorActor::reload
   * is a legacy API which is for instance used from web-ext.
   *
   * bypassCache has no impact for addon reloads.
   */
  reloadDescriptor({ bypassCache }) {
    return this.reload();
  }

  async reload() {
    await this.addon.reload();
    return {};
  }

  async terminateBackgroundScript() {
    await lazy.ExtensionParent.DebugUtils.terminateBackgroundScript(
      this.addonId
    );
  }

  // This function will be called from RootActor in case that the devtools client
  // retrieves list of addons with `iconDataURL` option.
  async loadIconDataURL() {
    this._iconDataURL = await this.getIconDataURL();
  }

  async getIconDataURL() {
    if (!this.addon.iconURL) {
      return null;
    }

    const xhr = new XMLHttpRequest();
    xhr.responseType = "blob";
    xhr.open("GET", this.addon.iconURL, true);

    if (this.addon.iconURL.toLowerCase().endsWith(".svg")) {
      // Maybe SVG, thus force to change mime type.
      xhr.overrideMimeType("image/svg+xml");
    }

    try {
      const blob = await new Promise((resolve, reject) => {
        xhr.onload = () => resolve(xhr.response);
        xhr.onerror = reject;
        xhr.send();
      });

      const reader = new FileReader();
      return await new Promise((resolve, reject) => {
        reader.onloadend = () => resolve(reader.result);
        reader.onerror = reject;
        reader.readAsDataURL(blob);
      });
    } catch (_) {
      console.warn(`Failed to create data url from [${this.addon.iconURL}]`);
      return null;
    }
  }

  // Private Methods
  _getBackgroundScriptStatus() {
    const isRunning = lazy.ExtensionParent.DebugUtils.isBackgroundScriptRunning(
      this.addonId
    );
    // The background script status doesn't apply to this addon (e.g. the addon
    // type doesn't have any code, like staticthemes/langpacks/dictionaries, or
    // the extension does not have a background script at all).
    if (isRunning === undefined) {
      return undefined;
    }

    return isRunning ? BGSCRIPT_STATUSES.RUNNING : BGSCRIPT_STATUSES.STOPPED;
  }

  get _mm() {
    return (
      this._browser &&
      (this._browser.messageManager || this._browser.frameLoader.messageManager)
    );
  }

  /**
   * Handle the child actor exit.
   */
  _onChildExit(msg) {
    if (msg.json.actor !== this._childActorID) {
      return;
    }

    this.destroy();
  }

  // AddonManagerListener callbacks.
  onInstalled(addon) {
    if (addon.id != this.addonId) {
      return;
    }

    // Update the AddonManager's addon object on reload/update.
    this.addon = addon;
  }

  onUninstalled(addon) {
    if (addon != this.addon) {
      return;
    }

    this.destroy();
  }

  destroy() {
    lazy.AddonManager.removeAddonListener(this);

    this.addon = null;
    if (this._mm) {
      this._mm.removeMessageListener(
        "debug:webext_child_exit",
        this._onChildExit
      );

      this._mm.sendAsyncMessage("debug:webext_parent_exit", {
        actor: this._childActorID,
      });

      lazy.ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this);
    }

    this._browser = null;
    this._childActorID = null;

    this.emit("descriptor-destroyed");

    super.destroy();
  }
}

exports.WebExtensionDescriptorActor = WebExtensionDescriptorActor;