summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts/inspector.js
blob: 116993078b5c3dbe2488f43fd1c57e730ac884a2 (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
/* 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 Telemetry = require("resource://devtools/client/shared/telemetry.js");
const {
  FrontClassWithSpec,
  registerFront,
} = require("resource://devtools/shared/protocol.js");
const {
  inspectorSpec,
} = require("resource://devtools/shared/specs/inspector.js");

loader.lazyRequireGetter(
  this,
  "captureScreenshot",
  "resource://devtools/client/shared/screenshot.js",
  true
);

const TELEMETRY_EYEDROPPER_OPENED = "DEVTOOLS_EYEDROPPER_OPENED_COUNT";
const TELEMETRY_EYEDROPPER_OPENED_MENU =
  "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT";
const SHOW_ALL_ANONYMOUS_CONTENT_PREF =
  "devtools.inspector.showAllAnonymousContent";

const telemetry = new Telemetry();

/**
 * Client side of the inspector actor, which is used to create
 * inspector-related actors, including the walker.
 */
class InspectorFront extends FrontClassWithSpec(inspectorSpec) {
  constructor(client, targetFront, parentFront) {
    super(client, targetFront, parentFront);

    this._client = client;
    this._highlighters = new Map();

    // Attribute name from which to retrieve the actorID out of the target actor's form
    this.formAttributeName = "inspectorActor";

    // Map of highlighter types to unsettled promises to create a highlighter of that type
    this._pendingGetHighlighterMap = new Map();

    this.noopStylesheetListener = () => {};
  }

  // async initialization
  async initialize() {
    if (this.initialized) {
      return this.initialized;
    }

    // Watch STYLESHEET resources to fill the ResourceCommand cache.
    // StyleRule front's `get parentStyleSheet()` will query the cache to
    // retrieve the resource corresponding to the parent stylesheet of a rule.
    const { resourceCommand } = this.targetFront.commands;
    // Backup resourceCommand, targetFront.commands might be null in `destroy`.
    this.resourceCommand = resourceCommand;
    await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
      onAvailable: this.noopStylesheetListener,
    });

    // Bail out if the inspector is closed while watchResources was pending
    if (this.isDestroyed()) {
      return null;
    }

    this.initialized = await Promise.all([
      this._getWalker(),
      this._getPageStyle(),
    ]);

    return this.initialized;
  }

  async _getWalker() {
    const showAllAnonymousContent = Services.prefs.getBoolPref(
      SHOW_ALL_ANONYMOUS_CONTENT_PREF
    );
    this.walker = await this.getWalker({
      showAllAnonymousContent,
    });

    // We need to reparent the RootNode of remote iframe Walkers
    // so that their parent is the NodeFront of the <iframe>
    // element, coming from another process/target/WalkerFront.
    await this.walker.reparentRemoteFrame();
  }

  hasHighlighter(type) {
    return this._highlighters.has(type);
  }

  async _getPageStyle() {
    this.pageStyle = await super.getPageStyle();
  }

  async getCompatibilityFront() {
    if (!this._compatibility) {
      this._compatibility = await super.getCompatibility();
    }

    return this._compatibility;
  }

  destroy() {
    if (this.isDestroyed()) {
      return;
    }
    this._compatibility = null;

    const { resourceCommand } = this;
    resourceCommand.unwatchResources([resourceCommand.TYPES.STYLESHEET], {
      onAvailable: this.noopStylesheetListener,
    });
    this.resourceCommand = null;

    this.walker = null;

    // CustomHighlighter fronts are managed by InspectorFront and so will be
    // automatically destroyed. But we have to clear the `_highlighters`
    // Map as well as explicitly call `finalize` request on all of them.
    this.destroyHighlighters();
    super.destroy();
  }

  destroyHighlighters() {
    for (const type of this._highlighters.keys()) {
      if (this._highlighters.has(type)) {
        const highlighter = this._highlighters.get(type);
        if (!highlighter.isDestroyed()) {
          highlighter.finalize();
        }
        this._highlighters.delete(type);
      }
    }
  }

  async getHighlighterByType(typeName) {
    let highlighter = null;
    try {
      highlighter = await super.getHighlighterByType(typeName);
    } catch (_) {
      throw new Error(
        "The target doesn't support " +
          `creating highlighters by types or ${typeName} is unknown`
      );
    }
    return highlighter;
  }

  getKnownHighlighter(type) {
    return this._highlighters.get(type);
  }

  /**
   * Return a highlighter instance of the given type.
   * If an instance was previously created, return it. Else, create and return a new one.
   *
   * Store a promise for the request to create a new highlighter. If another request
   * comes in before that promise is resolved, wait for it to resolve and return the
   * highlighter instance it resolved with instead of creating a new request.
   *
   * @param  {String} type
   *         Highlighter type
   * @return {Promise}
   *         Promise which resolves with a highlighter instance of the given type
   */
  async getOrCreateHighlighterByType(type) {
    let front = this._highlighters.get(type);
    let pendingGetHighlighter = this._pendingGetHighlighterMap.get(type);

    if (!front && !pendingGetHighlighter) {
      pendingGetHighlighter = (async () => {
        const highlighter = await this.getHighlighterByType(type);
        this._highlighters.set(type, highlighter);
        this._pendingGetHighlighterMap.delete(type);
        return highlighter;
      })();

      this._pendingGetHighlighterMap.set(type, pendingGetHighlighter);
    }

    if (pendingGetHighlighter) {
      front = await pendingGetHighlighter;
    }

    return front;
  }

  async pickColorFromPage(options) {
    let screenshot = null;

    // @backward-compat { version 87 } ScreenshotContentActor was only added in 87.
    //                  When connecting to older server, the eyedropper will  use drawWindow
    //                  to retrieve the screenshot of the page (that's a decent fallback,
    //                  even if it doesn't handle remote frames).
    if (this.targetFront.hasActor("screenshotContent")) {
      try {
        // We use the screenshot actors as it can retrieve an image of the current viewport,
        // handling remote frame if need be.
        const { data } = await captureScreenshot(this.targetFront, {
          browsingContextID: this.targetFront.browsingContextID,
          disableFlash: true,
          ignoreDprForFileScale: true,
        });
        screenshot = data;
      } catch (e) {
        // We simply log the error and still call pickColorFromPage as it will default to
        // use drawWindow in order to get the screenshot of the page (that's a decent
        // fallback, even if it doesn't handle remote frames).
        console.error(
          "Error occured when taking a screenshot for the eyedropper",
          e
        );
      }
    }

    await super.pickColorFromPage({
      ...options,
      screenshot,
    });

    if (options?.fromMenu) {
      telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED_MENU).add(true);
    } else {
      telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED).add(true);
    }
  }

  /**
   * Given a node grip, return a NodeFront on the right context.
   *
   * @param {Object} grip: The node grip.
   * @returns {Promise<NodeFront|null>} A promise that resolves with  a NodeFront or null
   *                                    if the NodeFront couldn't be created/retrieved.
   */
  async getNodeFrontFromNodeGrip(grip) {
    return this.getNodeActorFromContentDomReference(grip.contentDomReference);
  }

  async getNodeActorFromContentDomReference(contentDomReference) {
    const { browsingContextId } = contentDomReference;
    // If the contentDomReference lives in the same browsing context id than the
    // current one, we can directly use the current walker.
    if (this.targetFront.browsingContextID === browsingContextId) {
      return this.walker.getNodeActorFromContentDomReference(
        contentDomReference
      );
    }

    // If the contentDomReference has a different browsing context than the current one,
    // we are either in Fission or in the Multiprocess Browser Toolbox, so we need to
    // retrieve the walker of the WindowGlobalTarget.
    // Get the target for this remote frame element

    // Tab and Process Descriptors expose a Watcher, which should be used to
    // fetch the node's target.
    let target;
    const { watcherFront } = this.targetFront.commands;
    if (watcherFront) {
      target = await watcherFront.getWindowGlobalTarget(browsingContextId);
    } else {
      // For descriptors which don't expose a watcher (e.g. WebExtension)
      // we used to call RootActor::getBrowsingContextDescriptor, but it was
      // removed in FF77.
      // Support for watcher in WebExtension descriptors is Bug 1644341.
      throw new Error(
        `Unable to call getNodeActorFromContentDomReference for ${this.targetFront.actorID}`
      );
    }
    const { walker } = await target.getFront("inspector");
    return walker.getNodeActorFromContentDomReference(contentDomReference);
  }
}

exports.InspectorFront = InspectorFront;
registerFront(InspectorFront);