1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/actors/stylesheets.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1055 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +let { components, Cc, Ci, Cu } = require("chrome"); 1.11 +let Services = require("Services"); 1.12 + 1.13 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.14 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.15 +Cu.import("resource://gre/modules/FileUtils.jsm"); 1.16 +Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); 1.17 +Cu.import("resource://gre/modules/Task.jsm"); 1.18 + 1.19 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.20 +const events = require("sdk/event/core"); 1.21 +const protocol = require("devtools/server/protocol"); 1.22 +const {Arg, Option, method, RetVal, types} = protocol; 1.23 +const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); 1.24 + 1.25 +loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); 1.26 + 1.27 +let TRANSITION_CLASS = "moz-styleeditor-transitioning"; 1.28 +let TRANSITION_DURATION_MS = 500; 1.29 +let TRANSITION_BUFFER_MS = 1000; 1.30 +let TRANSITION_RULE = "\ 1.31 +:root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\ 1.32 +transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ 1.33 +transition-delay: 0ms !important;\ 1.34 +transition-timing-function: ease-out !important;\ 1.35 +transition-property: all !important;\ 1.36 +}"; 1.37 + 1.38 +let LOAD_ERROR = "error-load"; 1.39 + 1.40 +exports.register = function(handle) { 1.41 + handle.addTabActor(StyleSheetsActor, "styleSheetsActor"); 1.42 + handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor"); 1.43 +}; 1.44 + 1.45 +exports.unregister = function(handle) { 1.46 + handle.removeTabActor(StyleSheetsActor); 1.47 + handle.removeGlobalActor(StyleSheetsActor); 1.48 +}; 1.49 + 1.50 +types.addActorType("stylesheet"); 1.51 +types.addActorType("originalsource"); 1.52 + 1.53 +/** 1.54 + * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the 1.55 + * stylesheets of a document. 1.56 + */ 1.57 +let StyleSheetsActor = protocol.ActorClass({ 1.58 + typeName: "stylesheets", 1.59 + 1.60 + /** 1.61 + * The window we work with, taken from the parent actor. 1.62 + */ 1.63 + get window() this.parentActor.window, 1.64 + 1.65 + /** 1.66 + * The current content document of the window we work with. 1.67 + */ 1.68 + get document() this.window.document, 1.69 + 1.70 + form: function() 1.71 + { 1.72 + return { actor: this.actorID }; 1.73 + }, 1.74 + 1.75 + initialize: function (conn, tabActor) { 1.76 + protocol.Actor.prototype.initialize.call(this, null); 1.77 + 1.78 + this.parentActor = tabActor; 1.79 + 1.80 + // keep a map of sheets-to-actors so we don't create two actors for one sheet 1.81 + this._sheets = new Map(); 1.82 + }, 1.83 + 1.84 + /** 1.85 + * Destroy the current StyleSheetsActor instance. 1.86 + */ 1.87 + destroy: function() 1.88 + { 1.89 + this._sheets.clear(); 1.90 + }, 1.91 + 1.92 + /** 1.93 + * Protocol method for getting a list of StyleSheetActors representing 1.94 + * all the style sheets in this document. 1.95 + */ 1.96 + getStyleSheets: method(function() { 1.97 + let deferred = promise.defer(); 1.98 + 1.99 + let window = this.window; 1.100 + var domReady = () => { 1.101 + window.removeEventListener("DOMContentLoaded", domReady, true); 1.102 + this._addAllStyleSheets().then(deferred.resolve, Cu.reportError); 1.103 + }; 1.104 + 1.105 + if (window.document.readyState === "loading") { 1.106 + window.addEventListener("DOMContentLoaded", domReady, true); 1.107 + } else { 1.108 + domReady(); 1.109 + } 1.110 + 1.111 + return deferred.promise; 1.112 + }, { 1.113 + request: {}, 1.114 + response: { styleSheets: RetVal("array:stylesheet") } 1.115 + }), 1.116 + 1.117 + /** 1.118 + * Add all the stylesheets in this document and its subframes. 1.119 + * Assumes the document is loaded. 1.120 + * 1.121 + * @return {Promise} 1.122 + * Promise that resolves with an array of StyleSheetActors 1.123 + */ 1.124 + _addAllStyleSheets: function() { 1.125 + return Task.spawn(function() { 1.126 + let documents = [this.document]; 1.127 + let actors = []; 1.128 + 1.129 + for (let doc of documents) { 1.130 + let sheets = yield this._addStyleSheets(doc.styleSheets); 1.131 + actors = actors.concat(sheets); 1.132 + 1.133 + // Recursively handle style sheets of the documents in iframes. 1.134 + for (let iframe of doc.getElementsByTagName("iframe")) { 1.135 + if (iframe.contentDocument) { 1.136 + // Sometimes, iframes don't have any document, like the 1.137 + // one that are over deeply nested (bug 285395) 1.138 + documents.push(iframe.contentDocument); 1.139 + } 1.140 + } 1.141 + } 1.142 + throw new Task.Result(actors); 1.143 + }.bind(this)); 1.144 + }, 1.145 + 1.146 + /** 1.147 + * Add all the stylesheets to the map and create an actor for each one 1.148 + * if not already created. 1.149 + * 1.150 + * @param {[DOMStyleSheet]} styleSheets 1.151 + * Stylesheets to add 1.152 + * 1.153 + * @return {Promise} 1.154 + * Promise that resolves to an array of StyleSheetActors 1.155 + */ 1.156 + _addStyleSheets: function(styleSheets) 1.157 + { 1.158 + return Task.spawn(function() { 1.159 + let actors = []; 1.160 + for (let i = 0; i < styleSheets.length; i++) { 1.161 + let actor = this._createStyleSheetActor(styleSheets[i]); 1.162 + actors.push(actor); 1.163 + 1.164 + // Get all sheets, including imported ones 1.165 + let imports = yield this._getImported(actor); 1.166 + actors = actors.concat(imports); 1.167 + } 1.168 + throw new Task.Result(actors); 1.169 + }.bind(this)); 1.170 + }, 1.171 + 1.172 + /** 1.173 + * Get all the stylesheets @imported from a stylesheet. 1.174 + * 1.175 + * @param {DOMStyleSheet} styleSheet 1.176 + * Style sheet to search 1.177 + * @return {Promise} 1.178 + * A promise that resolves with an array of StyleSheetActors 1.179 + */ 1.180 + _getImported: function(styleSheet) { 1.181 + return Task.spawn(function() { 1.182 + let rules = yield styleSheet.getCSSRules(); 1.183 + let imported = []; 1.184 + 1.185 + for (let i = 0; i < rules.length; i++) { 1.186 + let rule = rules[i]; 1.187 + if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { 1.188 + // Associated styleSheet may be null if it has already been seen due 1.189 + // to duplicate @imports for the same URL. 1.190 + if (!rule.styleSheet) { 1.191 + continue; 1.192 + } 1.193 + let actor = this._createStyleSheetActor(rule.styleSheet); 1.194 + imported.push(actor); 1.195 + 1.196 + // recurse imports in this stylesheet as well 1.197 + let children = yield this._getImported(actor); 1.198 + imported = imported.concat(children); 1.199 + } 1.200 + else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { 1.201 + // @import rules must precede all others except @charset 1.202 + break; 1.203 + } 1.204 + } 1.205 + 1.206 + throw new Task.Result(imported); 1.207 + }.bind(this)); 1.208 + }, 1.209 + 1.210 + /** 1.211 + * Create a new actor for a style sheet, if it hasn't already been created. 1.212 + * 1.213 + * @param {DOMStyleSheet} styleSheet 1.214 + * The style sheet to create an actor for. 1.215 + * @return {StyleSheetActor} 1.216 + * The actor for this style sheet 1.217 + */ 1.218 + _createStyleSheetActor: function(styleSheet) 1.219 + { 1.220 + if (this._sheets.has(styleSheet)) { 1.221 + return this._sheets.get(styleSheet); 1.222 + } 1.223 + let actor = new StyleSheetActor(styleSheet, this); 1.224 + 1.225 + this.manage(actor); 1.226 + this._sheets.set(styleSheet, actor); 1.227 + 1.228 + return actor; 1.229 + }, 1.230 + 1.231 + /** 1.232 + * Clear all the current stylesheet actors in map. 1.233 + */ 1.234 + _clearStyleSheetActors: function() { 1.235 + for (let actor in this._sheets) { 1.236 + this.unmanage(this._sheets[actor]); 1.237 + } 1.238 + this._sheets.clear(); 1.239 + }, 1.240 + 1.241 + /** 1.242 + * Create a new style sheet in the document with the given text. 1.243 + * Return an actor for it. 1.244 + * 1.245 + * @param {object} request 1.246 + * Debugging protocol request object, with 'text property' 1.247 + * @return {object} 1.248 + * Object with 'styelSheet' property for form on new actor. 1.249 + */ 1.250 + addStyleSheet: method(function(text) { 1.251 + let parent = this.document.documentElement; 1.252 + let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); 1.253 + style.setAttribute("type", "text/css"); 1.254 + 1.255 + if (text) { 1.256 + style.appendChild(this.document.createTextNode(text)); 1.257 + } 1.258 + parent.appendChild(style); 1.259 + 1.260 + let actor = this._createStyleSheetActor(style.sheet); 1.261 + return actor; 1.262 + }, { 1.263 + request: { text: Arg(0, "string") }, 1.264 + response: { styleSheet: RetVal("stylesheet") } 1.265 + }) 1.266 +}); 1.267 + 1.268 +/** 1.269 + * The corresponding Front object for the StyleSheetsActor. 1.270 + */ 1.271 +let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, { 1.272 + initialize: function(client, tabForm) { 1.273 + protocol.Front.prototype.initialize.call(this, client); 1.274 + this.actorID = tabForm.styleSheetsActor; 1.275 + 1.276 + client.addActorPool(this); 1.277 + this.manage(this); 1.278 + } 1.279 +}); 1.280 + 1.281 +/** 1.282 + * A StyleSheetActor represents a stylesheet on the server. 1.283 + */ 1.284 +let StyleSheetActor = protocol.ActorClass({ 1.285 + typeName: "stylesheet", 1.286 + 1.287 + events: { 1.288 + "property-change" : { 1.289 + type: "propertyChange", 1.290 + property: Arg(0, "string"), 1.291 + value: Arg(1, "json") 1.292 + }, 1.293 + "style-applied" : { 1.294 + type: "styleApplied" 1.295 + } 1.296 + }, 1.297 + 1.298 + /* List of original sources that generated this stylesheet */ 1.299 + _originalSources: null, 1.300 + 1.301 + toString: function() { 1.302 + return "[StyleSheetActor " + this.actorID + "]"; 1.303 + }, 1.304 + 1.305 + /** 1.306 + * Window of target 1.307 + */ 1.308 + get window() this._window || this.parentActor.window, 1.309 + 1.310 + /** 1.311 + * Document of target. 1.312 + */ 1.313 + get document() this.window.document, 1.314 + 1.315 + /** 1.316 + * URL of underlying stylesheet. 1.317 + */ 1.318 + get href() this.rawSheet.href, 1.319 + 1.320 + /** 1.321 + * Retrieve the index (order) of stylesheet in the document. 1.322 + * 1.323 + * @return number 1.324 + */ 1.325 + get styleSheetIndex() 1.326 + { 1.327 + if (this._styleSheetIndex == -1) { 1.328 + for (let i = 0; i < this.document.styleSheets.length; i++) { 1.329 + if (this.document.styleSheets[i] == this.rawSheet) { 1.330 + this._styleSheetIndex = i; 1.331 + break; 1.332 + } 1.333 + } 1.334 + } 1.335 + return this._styleSheetIndex; 1.336 + }, 1.337 + 1.338 + initialize: function(aStyleSheet, aParentActor, aWindow) { 1.339 + protocol.Actor.prototype.initialize.call(this, null); 1.340 + 1.341 + this.rawSheet = aStyleSheet; 1.342 + this.parentActor = aParentActor; 1.343 + this.conn = this.parentActor.conn; 1.344 + 1.345 + this._window = aWindow; 1.346 + 1.347 + // text and index are unknown until source load 1.348 + this.text = null; 1.349 + this._styleSheetIndex = -1; 1.350 + 1.351 + this._transitionRefCount = 0; 1.352 + }, 1.353 + 1.354 + /** 1.355 + * Get the raw stylesheet's cssRules once the sheet has been loaded. 1.356 + * 1.357 + * @return {Promise} 1.358 + * Promise that resolves with a CSSRuleList 1.359 + */ 1.360 + getCSSRules: function() { 1.361 + let rules; 1.362 + try { 1.363 + rules = this.rawSheet.cssRules; 1.364 + } 1.365 + catch (e) { 1.366 + // sheet isn't loaded yet 1.367 + } 1.368 + 1.369 + if (rules) { 1.370 + return promise.resolve(rules); 1.371 + } 1.372 + 1.373 + let ownerNode = this.rawSheet.ownerNode; 1.374 + if (!ownerNode) { 1.375 + return promise.resolve([]); 1.376 + } 1.377 + 1.378 + if (this._cssRules) { 1.379 + return this._cssRules; 1.380 + } 1.381 + 1.382 + let deferred = promise.defer(); 1.383 + 1.384 + let onSheetLoaded = function(event) { 1.385 + ownerNode.removeEventListener("load", onSheetLoaded, false); 1.386 + 1.387 + deferred.resolve(this.rawSheet.cssRules); 1.388 + }.bind(this); 1.389 + 1.390 + ownerNode.addEventListener("load", onSheetLoaded, false); 1.391 + 1.392 + // cache so we don't add many listeners if this is called multiple times. 1.393 + this._cssRules = deferred.promise; 1.394 + 1.395 + return this._cssRules; 1.396 + }, 1.397 + 1.398 + /** 1.399 + * Get the current state of the actor 1.400 + * 1.401 + * @return {object} 1.402 + * With properties of the underlying stylesheet, plus 'text', 1.403 + * 'styleSheetIndex' and 'parentActor' if it's @imported 1.404 + */ 1.405 + form: function(detail) { 1.406 + if (detail === "actorid") { 1.407 + return this.actorID; 1.408 + } 1.409 + 1.410 + let docHref; 1.411 + let ownerNode = this.rawSheet.ownerNode; 1.412 + if (ownerNode) { 1.413 + if (ownerNode instanceof Ci.nsIDOMHTMLDocument) { 1.414 + docHref = ownerNode.location.href; 1.415 + } 1.416 + else if (ownerNode.ownerDocument && ownerNode.ownerDocument.location) { 1.417 + docHref = ownerNode.ownerDocument.location.href; 1.418 + } 1.419 + } 1.420 + 1.421 + let form = { 1.422 + actor: this.actorID, // actorID is set when this actor is added to a pool 1.423 + href: this.href, 1.424 + nodeHref: docHref, 1.425 + disabled: this.rawSheet.disabled, 1.426 + title: this.rawSheet.title, 1.427 + system: !CssLogic.isContentStylesheet(this.rawSheet), 1.428 + styleSheetIndex: this.styleSheetIndex 1.429 + } 1.430 + 1.431 + try { 1.432 + form.ruleCount = this.rawSheet.cssRules.length; 1.433 + } 1.434 + catch(e) { 1.435 + // stylesheet had an @import rule that wasn't loaded yet 1.436 + this.getCSSRules().then(() => { 1.437 + this._notifyPropertyChanged("ruleCount"); 1.438 + }); 1.439 + } 1.440 + return form; 1.441 + }, 1.442 + 1.443 + /** 1.444 + * Toggle the disabled property of the style sheet 1.445 + * 1.446 + * @return {object} 1.447 + * 'disabled' - the disabled state after toggling. 1.448 + */ 1.449 + toggleDisabled: method(function() { 1.450 + this.rawSheet.disabled = !this.rawSheet.disabled; 1.451 + this._notifyPropertyChanged("disabled"); 1.452 + 1.453 + return this.rawSheet.disabled; 1.454 + }, { 1.455 + response: { disabled: RetVal("boolean")} 1.456 + }), 1.457 + 1.458 + /** 1.459 + * Send an event notifying that a property of the stylesheet 1.460 + * has changed. 1.461 + * 1.462 + * @param {string} property 1.463 + * Name of the changed property 1.464 + */ 1.465 + _notifyPropertyChanged: function(property) { 1.466 + events.emit(this, "property-change", property, this.form()[property]); 1.467 + }, 1.468 + 1.469 + /** 1.470 + * Protocol method to get the text of this stylesheet. 1.471 + */ 1.472 + getText: method(function() { 1.473 + return this._getText().then((text) => { 1.474 + return new LongStringActor(this.conn, text || ""); 1.475 + }); 1.476 + }, { 1.477 + response: { 1.478 + text: RetVal("longstring") 1.479 + } 1.480 + }), 1.481 + 1.482 + /** 1.483 + * Fetch the text for this stylesheet from the cache or network. Return 1.484 + * cached text if it's already been fetched. 1.485 + * 1.486 + * @return {Promise} 1.487 + * Promise that resolves with a string text of the stylesheet. 1.488 + */ 1.489 + _getText: function() { 1.490 + if (this.text) { 1.491 + return promise.resolve(this.text); 1.492 + } 1.493 + 1.494 + if (!this.href) { 1.495 + // this is an inline <style> sheet 1.496 + let content = this.rawSheet.ownerNode.textContent; 1.497 + this.text = content; 1.498 + return promise.resolve(content); 1.499 + } 1.500 + 1.501 + let options = { 1.502 + window: this.window, 1.503 + charset: this._getCSSCharset() 1.504 + }; 1.505 + 1.506 + return fetch(this.href, options).then(({ content }) => { 1.507 + this.text = content; 1.508 + return content; 1.509 + }); 1.510 + }, 1.511 + 1.512 + /** 1.513 + * Protocol method to get the original source (actors) for this 1.514 + * stylesheet if it has uses source maps. 1.515 + */ 1.516 + getOriginalSources: method(function() { 1.517 + if (this._originalSources) { 1.518 + return promise.resolve(this._originalSources); 1.519 + } 1.520 + return this._fetchOriginalSources(); 1.521 + }, { 1.522 + request: {}, 1.523 + response: { 1.524 + originalSources: RetVal("nullable:array:originalsource") 1.525 + } 1.526 + }), 1.527 + 1.528 + /** 1.529 + * Fetch the original sources (actors) for this style sheet using its 1.530 + * source map. If they've already been fetched, returns cached array. 1.531 + * 1.532 + * @return {Promise} 1.533 + * Promise that resolves with an array of OriginalSourceActors 1.534 + */ 1.535 + _fetchOriginalSources: function() { 1.536 + this._clearOriginalSources(); 1.537 + this._originalSources = []; 1.538 + 1.539 + return this.getSourceMap().then((sourceMap) => { 1.540 + if (!sourceMap) { 1.541 + return null; 1.542 + } 1.543 + for (let url of sourceMap.sources) { 1.544 + let actor = new OriginalSourceActor(url, sourceMap, this); 1.545 + 1.546 + this.manage(actor); 1.547 + this._originalSources.push(actor); 1.548 + } 1.549 + return this._originalSources; 1.550 + }) 1.551 + }, 1.552 + 1.553 + /** 1.554 + * Get the SourceMapConsumer for this stylesheet's source map, if 1.555 + * it exists. Saves the consumer for later queries. 1.556 + * 1.557 + * @return {Promise} 1.558 + * A promise that resolves with a SourceMapConsumer, or null. 1.559 + */ 1.560 + getSourceMap: function() { 1.561 + if (this._sourceMap) { 1.562 + return this._sourceMap; 1.563 + } 1.564 + return this._fetchSourceMap(); 1.565 + }, 1.566 + 1.567 + /** 1.568 + * Fetch the source map for this stylesheet. 1.569 + * 1.570 + * @return {Promise} 1.571 + * A promise that resolves with a SourceMapConsumer, or null. 1.572 + */ 1.573 + _fetchSourceMap: function() { 1.574 + let deferred = promise.defer(); 1.575 + 1.576 + this._getText().then((content) => { 1.577 + let url = this._extractSourceMapUrl(content); 1.578 + if (!url) { 1.579 + // no source map for this stylesheet 1.580 + deferred.resolve(null); 1.581 + return; 1.582 + }; 1.583 + 1.584 + url = normalize(url, this.href); 1.585 + 1.586 + let map = fetch(url, { loadFromCache: false, window: this.window }) 1.587 + .then(({content}) => { 1.588 + let map = new SourceMapConsumer(content); 1.589 + this._setSourceMapRoot(map, url, this.href); 1.590 + this._sourceMap = promise.resolve(map); 1.591 + 1.592 + deferred.resolve(map); 1.593 + return map; 1.594 + }, deferred.reject); 1.595 + 1.596 + this._sourceMap = map; 1.597 + }, deferred.reject); 1.598 + 1.599 + return deferred.promise; 1.600 + }, 1.601 + 1.602 + /** 1.603 + * Clear and unmanage the original source actors for this stylesheet. 1.604 + */ 1.605 + _clearOriginalSources: function() { 1.606 + for (actor in this._originalSources) { 1.607 + this.unmanage(actor); 1.608 + } 1.609 + this._originalSources = null; 1.610 + }, 1.611 + 1.612 + /** 1.613 + * Sets the source map's sourceRoot to be relative to the source map url. 1.614 + */ 1.615 + _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) { 1.616 + const base = dirname( 1.617 + aAbsSourceMapURL.startsWith("data:") 1.618 + ? aScriptURL 1.619 + : aAbsSourceMapURL); 1.620 + aSourceMap.sourceRoot = aSourceMap.sourceRoot 1.621 + ? normalize(aSourceMap.sourceRoot, base) 1.622 + : base; 1.623 + }, 1.624 + 1.625 + /** 1.626 + * Get the source map url specified in the text of a stylesheet. 1.627 + * 1.628 + * @param {string} content 1.629 + * The text of the style sheet. 1.630 + * @return {string} 1.631 + * Url of source map. 1.632 + */ 1.633 + _extractSourceMapUrl: function(content) { 1.634 + var matches = /sourceMappingURL\=([^\s\*]*)/.exec(content); 1.635 + if (matches) { 1.636 + return matches[1]; 1.637 + } 1.638 + return null; 1.639 + }, 1.640 + 1.641 + /** 1.642 + * Protocol method that gets the location in the original source of a 1.643 + * line, column pair in this stylesheet, if its source mapped, otherwise 1.644 + * a promise of the same location. 1.645 + */ 1.646 + getOriginalLocation: method(function(line, column) { 1.647 + return this.getSourceMap().then((sourceMap) => { 1.648 + if (sourceMap) { 1.649 + return sourceMap.originalPositionFor({ line: line, column: column }); 1.650 + } 1.651 + return { 1.652 + source: this.href, 1.653 + line: line, 1.654 + column: column 1.655 + } 1.656 + }); 1.657 + }, { 1.658 + request: { 1.659 + line: Arg(0, "number"), 1.660 + column: Arg(1, "number") 1.661 + }, 1.662 + response: RetVal(types.addDictType("originallocationresponse", { 1.663 + source: "string", 1.664 + line: "number", 1.665 + column: "number" 1.666 + })) 1.667 + }), 1.668 + 1.669 + /** 1.670 + * Get the charset of the stylesheet according to the character set rules 1.671 + * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>. 1.672 + * 1.673 + * @param string channelCharset 1.674 + * Charset of the source string if set by the HTTP channel. 1.675 + */ 1.676 + _getCSSCharset: function(channelCharset) 1.677 + { 1.678 + // StyleSheet's charset can be specified from multiple sources 1.679 + if (channelCharset && channelCharset.length > 0) { 1.680 + // step 1 of syndata.html: charset given in HTTP header. 1.681 + return channelCharset; 1.682 + } 1.683 + 1.684 + let sheet = this.rawSheet; 1.685 + if (sheet) { 1.686 + // Do we have a @charset rule in the stylesheet? 1.687 + // step 2 of syndata.html (without the BOM check). 1.688 + if (sheet.cssRules) { 1.689 + let rules = sheet.cssRules; 1.690 + if (rules.length 1.691 + && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) { 1.692 + return rules.item(0).encoding; 1.693 + } 1.694 + } 1.695 + 1.696 + // step 3: charset attribute of <link> or <style> element, if it exists 1.697 + if (sheet.ownerNode && sheet.ownerNode.getAttribute) { 1.698 + let linkCharset = sheet.ownerNode.getAttribute("charset"); 1.699 + if (linkCharset != null) { 1.700 + return linkCharset; 1.701 + } 1.702 + } 1.703 + 1.704 + // step 4 (1 of 2): charset of referring stylesheet. 1.705 + let parentSheet = sheet.parentStyleSheet; 1.706 + if (parentSheet && parentSheet.cssRules && 1.707 + parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) { 1.708 + return parentSheet.cssRules[0].encoding; 1.709 + } 1.710 + 1.711 + // step 4 (2 of 2): charset of referring document. 1.712 + if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) { 1.713 + return sheet.ownerNode.ownerDocument.characterSet; 1.714 + } 1.715 + } 1.716 + 1.717 + // step 5: default to utf-8. 1.718 + return "UTF-8"; 1.719 + }, 1.720 + 1.721 + /** 1.722 + * Update the style sheet in place with new text. 1.723 + * 1.724 + * @param {object} request 1.725 + * 'text' - new text 1.726 + * 'transition' - whether to do CSS transition for change. 1.727 + */ 1.728 + update: method(function(text, transition) { 1.729 + DOMUtils.parseStyleSheet(this.rawSheet, text); 1.730 + 1.731 + this.text = text; 1.732 + 1.733 + this._notifyPropertyChanged("ruleCount"); 1.734 + 1.735 + if (transition) { 1.736 + this._insertTransistionRule(); 1.737 + } 1.738 + else { 1.739 + this._notifyStyleApplied(); 1.740 + } 1.741 + }, { 1.742 + request: { 1.743 + text: Arg(0, "string"), 1.744 + transition: Arg(1, "boolean") 1.745 + } 1.746 + }), 1.747 + 1.748 + /** 1.749 + * Insert a catch-all transition rule into the document. Set a timeout 1.750 + * to remove the rule after a certain time. 1.751 + */ 1.752 + _insertTransistionRule: function() { 1.753 + // Insert the global transition rule 1.754 + // Use a ref count to make sure we do not add it multiple times.. and remove 1.755 + // it only when all pending StyleSheets-generated transitions ended. 1.756 + if (this._transitionRefCount == 0) { 1.757 + this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length); 1.758 + this.document.documentElement.classList.add(TRANSITION_CLASS); 1.759 + } 1.760 + 1.761 + this._transitionRefCount++; 1.762 + 1.763 + // Set up clean up and commit after transition duration (+buffer) 1.764 + // @see _onTransitionEnd 1.765 + this.window.setTimeout(this._onTransitionEnd.bind(this), 1.766 + TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS); 1.767 + }, 1.768 + 1.769 + /** 1.770 + * This cleans up class and rule added for transition effect and then 1.771 + * notifies that the style has been applied. 1.772 + */ 1.773 + _onTransitionEnd: function() 1.774 + { 1.775 + if (--this._transitionRefCount == 0) { 1.776 + this.document.documentElement.classList.remove(TRANSITION_CLASS); 1.777 + this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1); 1.778 + } 1.779 + 1.780 + events.emit(this, "style-applied"); 1.781 + } 1.782 +}) 1.783 + 1.784 +/** 1.785 + * StyleSheetFront is the client-side counterpart to a StyleSheetActor. 1.786 + */ 1.787 +var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { 1.788 + initialize: function(conn, form) { 1.789 + protocol.Front.prototype.initialize.call(this, conn, form); 1.790 + 1.791 + this._onPropertyChange = this._onPropertyChange.bind(this); 1.792 + events.on(this, "property-change", this._onPropertyChange); 1.793 + }, 1.794 + 1.795 + destroy: function() { 1.796 + events.off(this, "property-change", this._onPropertyChange); 1.797 + 1.798 + protocol.Front.prototype.destroy.call(this); 1.799 + }, 1.800 + 1.801 + _onPropertyChange: function(property, value) { 1.802 + this._form[property] = value; 1.803 + }, 1.804 + 1.805 + form: function(form, detail) { 1.806 + if (detail === "actorid") { 1.807 + this.actorID = form; 1.808 + return; 1.809 + } 1.810 + this.actorID = form.actor; 1.811 + this._form = form; 1.812 + }, 1.813 + 1.814 + get href() this._form.href, 1.815 + get nodeHref() this._form.nodeHref, 1.816 + get disabled() !!this._form.disabled, 1.817 + get title() this._form.title, 1.818 + get isSystem() this._form.system, 1.819 + get styleSheetIndex() this._form.styleSheetIndex, 1.820 + get ruleCount() this._form.ruleCount 1.821 +}); 1.822 + 1.823 +/** 1.824 + * Actor representing an original source of a style sheet that was specified 1.825 + * in a source map. 1.826 + */ 1.827 +let OriginalSourceActor = protocol.ActorClass({ 1.828 + typeName: "originalsource", 1.829 + 1.830 + initialize: function(aUrl, aSourceMap, aParentActor) { 1.831 + protocol.Actor.prototype.initialize.call(this, null); 1.832 + 1.833 + this.url = aUrl; 1.834 + this.sourceMap = aSourceMap; 1.835 + this.parentActor = aParentActor; 1.836 + this.conn = this.parentActor.conn; 1.837 + 1.838 + this.text = null; 1.839 + }, 1.840 + 1.841 + form: function() { 1.842 + return { 1.843 + actor: this.actorID, // actorID is set when it's added to a pool 1.844 + url: this.url, 1.845 + relatedStyleSheet: this.parentActor.form() 1.846 + }; 1.847 + }, 1.848 + 1.849 + _getText: function() { 1.850 + if (this.text) { 1.851 + return promise.resolve(this.text); 1.852 + } 1.853 + let content = this.sourceMap.sourceContentFor(this.url); 1.854 + if (content) { 1.855 + this.text = content; 1.856 + return promise.resolve(content); 1.857 + } 1.858 + return fetch(this.url, { window: this.window }).then(({content}) => { 1.859 + this.text = content; 1.860 + return content; 1.861 + }); 1.862 + }, 1.863 + 1.864 + /** 1.865 + * Protocol method to get the text of this source. 1.866 + */ 1.867 + getText: method(function() { 1.868 + return this._getText().then((text) => { 1.869 + return new LongStringActor(this.conn, text || ""); 1.870 + }); 1.871 + }, { 1.872 + response: { 1.873 + text: RetVal("longstring") 1.874 + } 1.875 + }) 1.876 +}) 1.877 + 1.878 +/** 1.879 + * The client-side counterpart for an OriginalSourceActor. 1.880 + */ 1.881 +let OriginalSourceFront = protocol.FrontClass(OriginalSourceActor, { 1.882 + initialize: function(client, form) { 1.883 + protocol.Front.prototype.initialize.call(this, client, form); 1.884 + 1.885 + this.isOriginalSource = true; 1.886 + }, 1.887 + 1.888 + form: function(form, detail) { 1.889 + if (detail === "actorid") { 1.890 + this.actorID = form; 1.891 + return; 1.892 + } 1.893 + this.actorID = form.actor; 1.894 + this._form = form; 1.895 + }, 1.896 + 1.897 + get href() this._form.url, 1.898 + get url() this._form.url 1.899 +}); 1.900 + 1.901 + 1.902 +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { 1.903 + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); 1.904 +}); 1.905 + 1.906 +exports.StyleSheetsActor = StyleSheetsActor; 1.907 +exports.StyleSheetsFront = StyleSheetsFront; 1.908 + 1.909 +exports.StyleSheetActor = StyleSheetActor; 1.910 +exports.StyleSheetFront = StyleSheetFront; 1.911 + 1.912 + 1.913 +/** 1.914 + * Performs a request to load the desired URL and returns a promise. 1.915 + * 1.916 + * @param aURL String 1.917 + * The URL we will request. 1.918 + * @returns Promise 1.919 + * A promise of the document at that URL, as a string. 1.920 + */ 1.921 +function fetch(aURL, aOptions={ loadFromCache: true, window: null, 1.922 + charset: null}) { 1.923 + let deferred = promise.defer(); 1.924 + let scheme; 1.925 + let url = aURL.split(" -> ").pop(); 1.926 + let charset; 1.927 + let contentType; 1.928 + 1.929 + try { 1.930 + scheme = Services.io.extractScheme(url); 1.931 + } catch (e) { 1.932 + // In the xpcshell tests, the script url is the absolute path of the test 1.933 + // file, which will make a malformed URI error be thrown. Add the file 1.934 + // scheme prefix ourselves. 1.935 + url = "file://" + url; 1.936 + scheme = Services.io.extractScheme(url); 1.937 + } 1.938 + 1.939 + switch (scheme) { 1.940 + case "file": 1.941 + case "chrome": 1.942 + case "resource": 1.943 + try { 1.944 + NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) { 1.945 + if (!components.isSuccessCode(aStatus)) { 1.946 + deferred.reject(new Error("Request failed with status code = " 1.947 + + aStatus 1.948 + + " after NetUtil.asyncFetch for url = " 1.949 + + url)); 1.950 + return; 1.951 + } 1.952 + 1.953 + let source = NetUtil.readInputStreamToString(aStream, aStream.available()); 1.954 + contentType = aRequest.contentType; 1.955 + deferred.resolve(source); 1.956 + aStream.close(); 1.957 + }); 1.958 + } catch (ex) { 1.959 + deferred.reject(ex); 1.960 + } 1.961 + break; 1.962 + 1.963 + default: 1.964 + let channel; 1.965 + try { 1.966 + channel = Services.io.newChannel(url, null, null); 1.967 + } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") { 1.968 + // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but 1.969 + // newChannel won't be able to handle it. 1.970 + url = "file:///" + url; 1.971 + channel = Services.io.newChannel(url, null, null); 1.972 + } 1.973 + let chunks = []; 1.974 + let streamListener = { 1.975 + onStartRequest: function(aRequest, aContext, aStatusCode) { 1.976 + if (!components.isSuccessCode(aStatusCode)) { 1.977 + deferred.reject(new Error("Request failed with status code = " 1.978 + + aStatusCode 1.979 + + " in onStartRequest handler for url = " 1.980 + + url)); 1.981 + } 1.982 + }, 1.983 + onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { 1.984 + chunks.push(NetUtil.readInputStreamToString(aStream, aCount)); 1.985 + }, 1.986 + onStopRequest: function(aRequest, aContext, aStatusCode) { 1.987 + if (!components.isSuccessCode(aStatusCode)) { 1.988 + deferred.reject(new Error("Request failed with status code = " 1.989 + + aStatusCode 1.990 + + " in onStopRequest handler for url = " 1.991 + + url)); 1.992 + return; 1.993 + } 1.994 + 1.995 + charset = channel.contentCharset || charset; 1.996 + contentType = channel.contentType; 1.997 + deferred.resolve(chunks.join("")); 1.998 + } 1.999 + }; 1.1000 + 1.1001 + if (aOptions.window) { 1.1002 + // respect private browsing 1.1003 + channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor) 1.1004 + .getInterface(Ci.nsIWebNavigation) 1.1005 + .QueryInterface(Ci.nsIDocumentLoader) 1.1006 + .loadGroup; 1.1007 + } 1.1008 + channel.loadFlags = aOptions.loadFromCache 1.1009 + ? channel.LOAD_FROM_CACHE 1.1010 + : channel.LOAD_BYPASS_CACHE; 1.1011 + channel.asyncOpen(streamListener, null); 1.1012 + break; 1.1013 + } 1.1014 + 1.1015 + return deferred.promise.then(source => { 1.1016 + return { 1.1017 + content: convertToUnicode(source, charset), 1.1018 + contentType: contentType 1.1019 + }; 1.1020 + }); 1.1021 +} 1.1022 + 1.1023 +/** 1.1024 + * Convert a given string, encoded in a given character set, to unicode. 1.1025 + * 1.1026 + * @param string aString 1.1027 + * A string. 1.1028 + * @param string aCharset 1.1029 + * A character set. 1.1030 + */ 1.1031 +function convertToUnicode(aString, aCharset=null) { 1.1032 + // Decoding primitives. 1.1033 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.1034 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.1035 + try { 1.1036 + converter.charset = aCharset || "UTF-8"; 1.1037 + return converter.ConvertToUnicode(aString); 1.1038 + } catch(e) { 1.1039 + return aString; 1.1040 + } 1.1041 +} 1.1042 + 1.1043 +/** 1.1044 + * Normalize multiple relative paths towards the base paths on the right. 1.1045 + */ 1.1046 +function normalize(...aURLs) { 1.1047 + let base = Services.io.newURI(aURLs.pop(), null, null); 1.1048 + let url; 1.1049 + while ((url = aURLs.pop())) { 1.1050 + base = Services.io.newURI(url, null, base); 1.1051 + } 1.1052 + return base.spec; 1.1053 +} 1.1054 + 1.1055 +function dirname(aPath) { 1.1056 + return Services.io.newURI( 1.1057 + ".", null, Services.io.newURI(aPath, null, null)).spec; 1.1058 +}