summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/style-sheets.js
blob: 3b99590aca11813ade72e9e231dd8c0013e5d8b8 (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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
/* 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 protocol = require("devtools/shared/protocol");
const { LongStringActor } = require("devtools/server/actors/string");
const { styleSheetsSpec } = require("devtools/shared/specs/style-sheets");
const InspectorUtils = require("InspectorUtils");

const {
  TYPES,
  getResourceWatcher,
} = require("devtools/server/actors/resources/index");

loader.lazyRequireGetter(
  this,
  "UPDATE_GENERAL",
  "devtools/server/actors/style-sheet",
  true
);

/**
 * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the
 * stylesheets of a document.
 */
var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
  /**
   * The window we work with, taken from the parent actor.
   */
  get window() {
    return this.parentActor.window;
  },

  /**
   * The current content document of the window we work with.
   */
  get document() {
    return this.window.document;
  },

  initialize: function(conn, targetActor) {
    protocol.Actor.prototype.initialize.call(this, targetActor.conn);

    this.parentActor = targetActor;

    this._onApplicableStateChanged = this._onApplicableStateChanged.bind(this);
    this._onNewStyleSheetActor = this._onNewStyleSheetActor.bind(this);
    this._onWindowReady = this._onWindowReady.bind(this);
    this._transitionSheetLoaded = false;

    this.parentActor.on("stylesheet-added", this._onNewStyleSheetActor);
    this.parentActor.on("window-ready", this._onWindowReady);

    this.parentActor.chromeEventHandler.addEventListener(
      "StyleSheetApplicableStateChanged",
      this._onApplicableStateChanged,
      true
    );
  },

  getTraits() {
    return {
      traits: {},
    };
  },

  destroy: function() {
    for (const win of this.parentActor.windows) {
      // This flag only exists for devtools, so we are free to clear
      // it when we're done.
      win.document.styleSheetChangeEventsEnabled = false;
    }

    this.parentActor.off("stylesheet-added", this._onNewStyleSheetActor);
    this.parentActor.off("window-ready", this._onWindowReady);

    this.parentActor.chromeEventHandler.removeEventListener(
      "StyleSheetApplicableStateChanged",
      this._onApplicableStateChanged,
      true
    );

    protocol.Actor.prototype.destroy.call(this);
  },

  /**
   * Event handler that is called when a the target actor emits window-ready.
   *
   * @param {Event} evt
   *        The triggering event.
   */
  _onWindowReady: function(evt) {
    this._addStyleSheets(evt.window);
  },

  /**
   * Event handler that is called when a the target actor emits stylesheet-added.
   *
   * @param {StyleSheetActor} actor
   *        The new style sheet actor.
   */
  _onNewStyleSheetActor: function(actor) {
    const info = this._addingStyleSheetInfo?.get(actor.rawSheet);
    this._addingStyleSheetInfo?.delete(actor.rawSheet);

    // Forward it to the client side.
    this.emit(
      "stylesheet-added",
      actor,
      info ? info.isNew : false,
      info ? info.fileName : null
    );
  },

  /**
   * Protocol method for getting a list of StyleSheetActors representing
   * all the style sheets in this document.
   */
  async getStyleSheets() {
    let actors = [];

    const windows = this.parentActor.windows;
    for (const win of windows) {
      const sheets = await this._addStyleSheets(win);
      actors = actors.concat(sheets);
    }
    return actors;
  },

  /**
   * Check if we should be showing this stylesheet.
   *
   * @param {DOMCSSStyleSheet} sheet
   *        Stylesheet we're interested in
   *
   * @return boolean
   *         Whether the stylesheet should be listed.
   */
  _shouldListSheet: function(sheet) {
    // Special case about:PreferenceStyleSheet, as it is generated on the
    // fly and the URI is not registered with the about: handler.
    // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
    if (sheet.href?.toLowerCase() === "about:preferencestylesheet") {
      return false;
    }

    return true;
  },

  /**
   * Event handler that is called when the state of applicable of style sheet is changed.
   *
   * For now, StyleSheetApplicableStateChanged event will be called at following timings.
   * - Append <link> of stylesheet to document
   * - Append <style> to document
   * - Change disable attribute of stylesheet object
   * - Change disable attribute of <link> to false
   * When appending <link>, <style> or changing `disable` attribute to false, `applicable`
   * is passed as true. The other hand, when changing `disable` to true, this will be
   * false.
   * NOTE: For now, StyleSheetApplicableStateChanged will not be called when removing the
   *       link and style element.
   *
   * @param {StyleSheetApplicableStateChanged}
   *        The triggering event.
   */
  _onApplicableStateChanged: function({ applicable, stylesheet }) {
    if (
      // Have interest in applicable stylesheet only.
      applicable &&
      // No ownerNode means that this stylesheet is *not* associated to a DOM Element.
      stylesheet.ownerNode &&
      this._shouldListSheet(stylesheet) &&
      !this._haveAncestorWithSameURL(stylesheet)
    ) {
      this.parentActor.createStyleSheetActor(stylesheet);
    }
  },

  /**
   * Add all the stylesheets for the document in this window to the map and
   * create an actor for each one if not already created.
   *
   * @param {Window} win
   *        Window for which to add stylesheets
   *
   * @return {Promise}
   *         Promise that resolves to an array of StyleSheetActors
   */
  _addStyleSheets: function(win) {
    return async function() {
      const doc = win.document;
      // We have to set this flag in order to get the
      // StyleSheetApplicableStateChanged events.  See Document.webidl.
      doc.styleSheetChangeEventsEnabled = true;

      const documentOnly = !doc.nodePrincipal.isSystemPrincipal;
      const styleSheets = InspectorUtils.getAllStyleSheets(doc, documentOnly);

      let actors = [];
      for (let i = 0; i < styleSheets.length; i++) {
        const sheet = styleSheets[i];
        if (!this._shouldListSheet(sheet)) {
          continue;
        }

        const actor = this.parentActor.createStyleSheetActor(sheet);
        actors.push(actor);

        // Get all sheets, including imported ones
        const imports = await this._getImported(doc, actor);
        actors = actors.concat(imports);
      }
      return actors;
    }.bind(this)();
  },

  /**
   * Get all the stylesheets @imported from a stylesheet.
   *
   * @param  {Document} doc
   *         The document including the stylesheet
   * @param  {DOMStyleSheet} styleSheet
   *         Style sheet to search
   * @return {Promise}
   *         A promise that resolves with an array of StyleSheetActors
   */
  _getImported: function(doc, styleSheet) {
    return async function() {
      const rules = await styleSheet.getCSSRules();
      let imported = [];

      for (let i = 0; i < rules.length; i++) {
        const rule = rules[i];
        if (rule.type == CSSRule.IMPORT_RULE) {
          // With the Gecko style system, the associated styleSheet may be null
          // if it has already been seen because an import cycle for the same
          // URL.  With Stylo, the styleSheet will exist (which is correct per
          // the latest CSSOM spec), so we also need to check ancestors for the
          // same URL to avoid cycles.
          const sheet = rule.styleSheet;
          if (
            !sheet ||
            this._haveAncestorWithSameURL(sheet) ||
            !this._shouldListSheet(sheet)
          ) {
            continue;
          }
          const actor = this.parentActor.createStyleSheetActor(rule.styleSheet);
          imported.push(actor);

          // recurse imports in this stylesheet as well
          const children = await this._getImported(doc, actor);
          imported = imported.concat(children);
        } else if (rule.type != CSSRule.CHARSET_RULE) {
          // @import rules must precede all others except @charset
          break;
        }
      }

      return imported;
    }.bind(this)();
  },

  /**
   * Check all ancestors to see if this sheet's URL matches theirs as a way to
   * detect an import cycle.
   *
   * @param {DOMStyleSheet} sheet
   */
  _haveAncestorWithSameURL(sheet) {
    const sheetHref = sheet.href;
    while (sheet.parentStyleSheet) {
      if (sheet.parentStyleSheet.href == sheetHref) {
        return true;
      }
      sheet = sheet.parentStyleSheet;
    }
    return false;
  },

  /**
   * Create a new style sheet in the document with the given text.
   * Return an actor for it.
   *
   * @param  {object} request
   *         Debugging protocol request object, with 'text property'
   * @param  {string} fileName
   *         If the stylesheet adding is from file, `fileName` indicates the path.
   * @return {object}
   *         Object with 'styelSheet' property for form on new actor.
   */
  async addStyleSheet(text, fileName = null) {
    const styleSheetsWatcher = this._getStyleSheetsWatcher();
    if (styleSheetsWatcher) {
      await styleSheetsWatcher.addStyleSheet(this.document, text, fileName);
      return;
    }

    // Following code can be removed once we enable STYLESHEET resource on the watcher/server
    // side by default. For now it is being preffed off and we have to support the two
    // codepaths. Once enabled we will only support the stylesheet watcher codepath.
    const parent = this.document.documentElement;
    const style = this.document.createElementNS(
      "http://www.w3.org/1999/xhtml",
      "style"
    );
    style.setAttribute("type", "text/css");

    if (text) {
      style.appendChild(this.document.createTextNode(text));
    }
    parent.appendChild(style);

    // This is a bit convoluted.  The style sheet actor may be created
    // by a notification from platform.  In this case, we can't easily
    // pass the "new" flag through to createStyleSheetActor, so we set
    // a flag locally and check it before sending an event to the
    // client.  See |_onNewStyleSheetActor|.
    if (!this._addingStyleSheetInfo) {
      this._addingStyleSheetInfo = new WeakMap();
    }
    this._addingStyleSheetInfo.set(style.sheet, { isNew: true, fileName });

    const actor = this.parentActor.createStyleSheetActor(style.sheet);
    // eslint-disable-next-line consistent-return
    return actor;
  },

  _getStyleSheetActor(resourceId) {
    return this.parentActor._targetScopedActorPool.getActorByID(resourceId);
  },

  _getStyleSheetsWatcher() {
    return getResourceWatcher(this.parentActor, TYPES.STYLESHEET);
  },

  toggleDisabled(resourceId) {
    const styleSheetsWatcher = this._getStyleSheetsWatcher();
    if (styleSheetsWatcher) {
      return styleSheetsWatcher.toggleDisabled(resourceId);
    }

    // Following code can be removed once we enable STYLESHEET resource on the watcher/server
    // side by default. For now it is being preffed off and we have to support the two
    // codepaths. Once enabled we will only support the stylesheet watcher codepath.
    const actor = this._getStyleSheetActor(resourceId);
    return actor.toggleDisabled();
  },

  async getText(resourceId) {
    const styleSheetsWatcher = this._getStyleSheetsWatcher();
    if (styleSheetsWatcher) {
      const text = await styleSheetsWatcher.getText(resourceId);
      return new LongStringActor(this.conn, text || "");
    }

    // Following code can be removed once we enable STYLESHEET resource on the watcher/server
    // side by default. For now it is being preffed off and we have to support the two
    // codepaths. Once enabled we will only support the stylesheet watcher codepath.
    const actor = this._getStyleSheetActor(resourceId);
    return actor.getText();
  },

  update(resourceId, text, transition, cause = "") {
    const styleSheetsWatcher = this._getStyleSheetsWatcher();
    if (styleSheetsWatcher) {
      return styleSheetsWatcher.update(
        resourceId,
        text,
        transition,
        UPDATE_GENERAL,
        cause
      );
    }

    // Following code can be removed once we enable STYLESHEET resource on the watcher/server
    // side by default. For now it is being preffed off and we have to support the two
    // codepaths. Once enabled we will only support the stylesheet watcher codepath.
    const actor = this._getStyleSheetActor(resourceId);
    return actor.update(text, transition, UPDATE_GENERAL, cause);
  },
});

exports.StyleSheetsActor = StyleSheetsActor;