/* 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"; let { components, Cc, Ci, Cu } = require("chrome"); let Services = require("Services"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const events = require("sdk/event/core"); const protocol = require("devtools/server/protocol"); const {Arg, Option, method, RetVal, types} = protocol; const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); let TRANSITION_CLASS = "moz-styleeditor-transitioning"; let TRANSITION_DURATION_MS = 500; let TRANSITION_RULE = "\ :root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\ transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ transition-delay: 0ms !important;\ transition-timing-function: ease-out !important;\ transition-property: all !important;\ }"; let LOAD_ERROR = "error-load"; types.addActorType("old-stylesheet"); /** * Creates a StyleEditorActor. StyleEditorActor provides remote access to the * stylesheets of a document. */ let StyleEditorActor = exports.StyleEditorActor = protocol.ActorClass({ typeName: "styleeditor", /** * The window we work with, taken from the parent actor. */ get window() this.parentActor.window, /** * The current content document of the window we work with. */ get document() this.window.document, events: { "document-load" : { type: "documentLoad", styleSheets: Arg(0, "array:old-stylesheet") } }, form: function() { return { actor: this.actorID }; }, initialize: function (conn, tabActor) { protocol.Actor.prototype.initialize.call(this, null); this.parentActor = tabActor; // keep a map of sheets-to-actors so we don't create two actors for one sheet this._sheets = new Map(); }, /** * Destroy the current StyleEditorActor instance. */ destroy: function() { this._sheets.clear(); }, /** * Called by client when target navigates to a new document. * Adds load listeners to document. */ newDocument: method(function() { // delete previous document's actors this._clearStyleSheetActors(); // Note: listening for load won't be necessary once // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed if (this.document.readyState == "complete") { this._onDocumentLoaded(); } else { this.window.addEventListener("load", this._onDocumentLoaded, false); } return {}; }), /** * Event handler for document loaded event. Add actor for each stylesheet * and send an event notifying of the load */ _onDocumentLoaded: function(event) { if (event) { this.window.removeEventListener("load", this._onDocumentLoaded, false); } let documents = [this.document]; var forms = []; for (let doc of documents) { let sheetForms = this._addStyleSheets(doc.styleSheets); forms = forms.concat(sheetForms); // Recursively handle style sheets of the documents in iframes. for (let iframe of doc.getElementsByTagName("iframe")) { documents.push(iframe.contentDocument); } } events.emit(this, "document-load", forms); }, /** * Add all the stylesheets to the map and create an actor for each one * if not already created. Send event that there are new stylesheets. * * @param {[DOMStyleSheet]} styleSheets * Stylesheets to add * @return {[object]} * Array of actors for each StyleSheetActor created */ _addStyleSheets: function(styleSheets) { let sheets = []; for (let i = 0; i < styleSheets.length; i++) { let styleSheet = styleSheets[i]; sheets.push(styleSheet); // Get all sheets, including imported ones let imports = this._getImported(styleSheet); sheets = sheets.concat(imports); } let actors = sheets.map(this._createStyleSheetActor.bind(this)); return actors; }, /** * Create a new actor for a style sheet, if it hasn't already been created. * * @param {DOMStyleSheet} styleSheet * The style sheet to create an actor for. * @return {StyleSheetActor} * The actor for this style sheet */ _createStyleSheetActor: function(styleSheet) { if (this._sheets.has(styleSheet)) { return this._sheets.get(styleSheet); } let actor = new OldStyleSheetActor(styleSheet, this); this.manage(actor); this._sheets.set(styleSheet, actor); return actor; }, /** * Get all the stylesheets @imported from a stylesheet. * * @param {DOMStyleSheet} styleSheet * Style sheet to search * @return {array} * All the imported stylesheets */ _getImported: function(styleSheet) { let imported = []; for (let i = 0; i < styleSheet.cssRules.length; i++) { let rule = styleSheet.cssRules[i]; if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { // Associated styleSheet may be null if it has already been seen due to // duplicate @imports for the same URL. if (!rule.styleSheet) { continue; } imported.push(rule.styleSheet); // recurse imports in this stylesheet as well imported = imported.concat(this._getImported(rule.styleSheet)); } else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { // @import rules must precede all others except @charset break; } } return imported; }, /** * Clear all the current stylesheet actors in map. */ _clearStyleSheetActors: function() { for (let actor in this._sheets) { this.unmanage(this._sheets[actor]); } this._sheets.clear(); }, /** * 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' * @return {object} * Object with 'styelSheet' property for form on new actor. */ newStyleSheet: method(function(text) { let parent = this.document.documentElement; let 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); let actor = this._createStyleSheetActor(style.sheet); return actor; }, { request: { text: Arg(0, "string") }, response: { styleSheet: RetVal("old-stylesheet") } }) }); /** * The corresponding Front object for the StyleEditorActor. */ let StyleEditorFront = protocol.FrontClass(StyleEditorActor, { initialize: function(client, tabForm) { protocol.Front.prototype.initialize.call(this, client); this.actorID = tabForm.styleEditorActor; this.manage(this); }, getStyleSheets: function() { let deferred = promise.defer(); events.once(this, "document-load", (styleSheets) => { deferred.resolve(styleSheets); }); this.newDocument(); return deferred.promise; }, addStyleSheet: function(text) { return this.newStyleSheet(text); } }); /** * A StyleSheetActor represents a stylesheet on the server. */ let OldStyleSheetActor = protocol.ActorClass({ typeName: "old-stylesheet", events: { "property-change" : { type: "propertyChange", property: Arg(0, "string"), value: Arg(1, "json") }, "source-load" : { type: "sourceLoad", source: Arg(0, "string") }, "style-applied" : { type: "styleApplied" } }, toString: function() { return "[OldStyleSheetActor " + this.actorID + "]"; }, /** * Window of target */ get window() this._window || this.parentActor.window, /** * Document of target. */ get document() this.window.document, /** * URL of underlying stylesheet. */ get href() this.rawSheet.href, /** * Retrieve the index (order) of stylesheet in the document. * * @return number */ get styleSheetIndex() { if (this._styleSheetIndex == -1) { for (let i = 0; i < this.document.styleSheets.length; i++) { if (this.document.styleSheets[i] == this.rawSheet) { this._styleSheetIndex = i; break; } } } return this._styleSheetIndex; }, initialize: function(aStyleSheet, aParentActor, aWindow) { protocol.Actor.prototype.initialize.call(this, null); this.rawSheet = aStyleSheet; this.parentActor = aParentActor; this.conn = this.parentActor.conn; this._window = aWindow; // text and index are unknown until source load this.text = null; this._styleSheetIndex = -1; this._transitionRefCount = 0; // if this sheet has an @import, then it's rules are loaded async let ownerNode = this.rawSheet.ownerNode; if (ownerNode) { let onSheetLoaded = (event) => { ownerNode.removeEventListener("load", onSheetLoaded, false); this._notifyPropertyChanged("ruleCount"); }; ownerNode.addEventListener("load", onSheetLoaded, false); } }, /** * Get the current state of the actor * * @return {object} * With properties of the underlying stylesheet, plus 'text', * 'styleSheetIndex' and 'parentActor' if it's @imported */ form: function(detail) { if (detail === "actorid") { return this.actorID; } let docHref; if (this.rawSheet.ownerNode) { if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { docHref = this.rawSheet.ownerNode.location.href; } if (this.rawSheet.ownerNode.ownerDocument) { docHref = this.rawSheet.ownerNode.ownerDocument.location.href; } } let form = { actor: this.actorID, // actorID is set when this actor is added to a pool href: this.href, nodeHref: docHref, disabled: this.rawSheet.disabled, title: this.rawSheet.title, system: !CssLogic.isContentStylesheet(this.rawSheet), styleSheetIndex: this.styleSheetIndex } try { form.ruleCount = this.rawSheet.cssRules.length; } catch(e) { // stylesheet had an @import rule that wasn't loaded yet } return form; }, /** * Toggle the disabled property of the style sheet * * @return {object} * 'disabled' - the disabled state after toggling. */ toggleDisabled: method(function() { this.rawSheet.disabled = !this.rawSheet.disabled; this._notifyPropertyChanged("disabled"); return this.rawSheet.disabled; }, { response: { disabled: RetVal("boolean")} }), /** * Send an event notifying that a property of the stylesheet * has changed. * * @param {string} property * Name of the changed property */ _notifyPropertyChanged: function(property) { events.emit(this, "property-change", property, this.form()[property]); }, /** * Fetch the source of the style sheet from its URL. Send a "sourceLoad" * event when it's been fetched. */ fetchSource: method(function() { this._getText().then((content) => { events.emit(this, "source-load", this.text); }); }), /** * Fetch the text for this stylesheet from the cache or network. Return * cached text if it's already been fetched. * * @return {Promise} * Promise that resolves with a string text of the stylesheet. */ _getText: function() { if (this.text) { return promise.resolve(this.text); } if (!this.href) { // this is an inline