diff options
Diffstat (limited to 'devtools/server/actors/style-sheets.js')
-rw-r--r-- | devtools/server/actors/style-sheets.js | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/devtools/server/actors/style-sheets.js b/devtools/server/actors/style-sheets.js new file mode 100644 index 0000000000..3b99590aca --- /dev/null +++ b/devtools/server/actors/style-sheets.js @@ -0,0 +1,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; |