1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/actors/styleeditor.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,796 @@ 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 + 1.18 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.19 +const events = require("sdk/event/core"); 1.20 +const protocol = require("devtools/server/protocol"); 1.21 +const {Arg, Option, method, RetVal, types} = protocol; 1.22 +const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); 1.23 + 1.24 +loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); 1.25 + 1.26 +let TRANSITION_CLASS = "moz-styleeditor-transitioning"; 1.27 +let TRANSITION_DURATION_MS = 500; 1.28 +let TRANSITION_RULE = "\ 1.29 +:root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\ 1.30 +transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ 1.31 +transition-delay: 0ms !important;\ 1.32 +transition-timing-function: ease-out !important;\ 1.33 +transition-property: all !important;\ 1.34 +}"; 1.35 + 1.36 +let LOAD_ERROR = "error-load"; 1.37 + 1.38 +exports.register = function(handle) { 1.39 + handle.addTabActor(StyleEditorActor, "styleEditorActor"); 1.40 + handle.addGlobalActor(StyleEditorActor, "styleEditorActor"); 1.41 +}; 1.42 + 1.43 +exports.unregister = function(handle) { 1.44 + handle.removeTabActor(StyleEditorActor); 1.45 + handle.removeGlobalActor(StyleEditorActor); 1.46 +}; 1.47 + 1.48 +types.addActorType("old-stylesheet"); 1.49 + 1.50 +/** 1.51 + * Creates a StyleEditorActor. StyleEditorActor provides remote access to the 1.52 + * stylesheets of a document. 1.53 + */ 1.54 +let StyleEditorActor = protocol.ActorClass({ 1.55 + typeName: "styleeditor", 1.56 + 1.57 + /** 1.58 + * The window we work with, taken from the parent actor. 1.59 + */ 1.60 + get window() this.parentActor.window, 1.61 + 1.62 + /** 1.63 + * The current content document of the window we work with. 1.64 + */ 1.65 + get document() this.window.document, 1.66 + 1.67 + events: { 1.68 + "document-load" : { 1.69 + type: "documentLoad", 1.70 + styleSheets: Arg(0, "array:old-stylesheet") 1.71 + } 1.72 + }, 1.73 + 1.74 + form: function() 1.75 + { 1.76 + return { actor: this.actorID }; 1.77 + }, 1.78 + 1.79 + initialize: function (conn, tabActor) { 1.80 + protocol.Actor.prototype.initialize.call(this, null); 1.81 + 1.82 + this.parentActor = tabActor; 1.83 + 1.84 + // keep a map of sheets-to-actors so we don't create two actors for one sheet 1.85 + this._sheets = new Map(); 1.86 + }, 1.87 + 1.88 + /** 1.89 + * Destroy the current StyleEditorActor instance. 1.90 + */ 1.91 + destroy: function() 1.92 + { 1.93 + this._sheets.clear(); 1.94 + }, 1.95 + 1.96 + /** 1.97 + * Called by client when target navigates to a new document. 1.98 + * Adds load listeners to document. 1.99 + */ 1.100 + newDocument: method(function() { 1.101 + // delete previous document's actors 1.102 + this._clearStyleSheetActors(); 1.103 + 1.104 + // Note: listening for load won't be necessary once 1.105 + // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed 1.106 + if (this.document.readyState == "complete") { 1.107 + this._onDocumentLoaded(); 1.108 + } 1.109 + else { 1.110 + this.window.addEventListener("load", this._onDocumentLoaded, false); 1.111 + } 1.112 + return {}; 1.113 + }), 1.114 + 1.115 + /** 1.116 + * Event handler for document loaded event. Add actor for each stylesheet 1.117 + * and send an event notifying of the load 1.118 + */ 1.119 + _onDocumentLoaded: function(event) { 1.120 + if (event) { 1.121 + this.window.removeEventListener("load", this._onDocumentLoaded, false); 1.122 + } 1.123 + 1.124 + let documents = [this.document]; 1.125 + var forms = []; 1.126 + for (let doc of documents) { 1.127 + let sheetForms = this._addStyleSheets(doc.styleSheets); 1.128 + forms = forms.concat(sheetForms); 1.129 + // Recursively handle style sheets of the documents in iframes. 1.130 + for (let iframe of doc.getElementsByTagName("iframe")) { 1.131 + documents.push(iframe.contentDocument); 1.132 + } 1.133 + } 1.134 + 1.135 + events.emit(this, "document-load", forms); 1.136 + }, 1.137 + 1.138 + /** 1.139 + * Add all the stylesheets to the map and create an actor for each one 1.140 + * if not already created. Send event that there are new stylesheets. 1.141 + * 1.142 + * @param {[DOMStyleSheet]} styleSheets 1.143 + * Stylesheets to add 1.144 + * @return {[object]} 1.145 + * Array of actors for each StyleSheetActor created 1.146 + */ 1.147 + _addStyleSheets: function(styleSheets) 1.148 + { 1.149 + let sheets = []; 1.150 + for (let i = 0; i < styleSheets.length; i++) { 1.151 + let styleSheet = styleSheets[i]; 1.152 + sheets.push(styleSheet); 1.153 + 1.154 + // Get all sheets, including imported ones 1.155 + let imports = this._getImported(styleSheet); 1.156 + sheets = sheets.concat(imports); 1.157 + } 1.158 + let actors = sheets.map(this._createStyleSheetActor.bind(this)); 1.159 + 1.160 + return actors; 1.161 + }, 1.162 + 1.163 + /** 1.164 + * Create a new actor for a style sheet, if it hasn't already been created. 1.165 + * 1.166 + * @param {DOMStyleSheet} styleSheet 1.167 + * The style sheet to create an actor for. 1.168 + * @return {StyleSheetActor} 1.169 + * The actor for this style sheet 1.170 + */ 1.171 + _createStyleSheetActor: function(styleSheet) 1.172 + { 1.173 + if (this._sheets.has(styleSheet)) { 1.174 + return this._sheets.get(styleSheet); 1.175 + } 1.176 + let actor = new OldStyleSheetActor(styleSheet, this); 1.177 + 1.178 + this.manage(actor); 1.179 + this._sheets.set(styleSheet, actor); 1.180 + 1.181 + return actor; 1.182 + }, 1.183 + 1.184 + /** 1.185 + * Get all the stylesheets @imported from a stylesheet. 1.186 + * 1.187 + * @param {DOMStyleSheet} styleSheet 1.188 + * Style sheet to search 1.189 + * @return {array} 1.190 + * All the imported stylesheets 1.191 + */ 1.192 + _getImported: function(styleSheet) { 1.193 + let imported = []; 1.194 + 1.195 + for (let i = 0; i < styleSheet.cssRules.length; i++) { 1.196 + let rule = styleSheet.cssRules[i]; 1.197 + if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { 1.198 + // Associated styleSheet may be null if it has already been seen due to 1.199 + // duplicate @imports for the same URL. 1.200 + if (!rule.styleSheet) { 1.201 + continue; 1.202 + } 1.203 + imported.push(rule.styleSheet); 1.204 + 1.205 + // recurse imports in this stylesheet as well 1.206 + imported = imported.concat(this._getImported(rule.styleSheet)); 1.207 + } 1.208 + else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { 1.209 + // @import rules must precede all others except @charset 1.210 + break; 1.211 + } 1.212 + } 1.213 + return imported; 1.214 + }, 1.215 + 1.216 + /** 1.217 + * Clear all the current stylesheet actors in map. 1.218 + */ 1.219 + _clearStyleSheetActors: function() { 1.220 + for (let actor in this._sheets) { 1.221 + this.unmanage(this._sheets[actor]); 1.222 + } 1.223 + this._sheets.clear(); 1.224 + }, 1.225 + 1.226 + /** 1.227 + * Create a new style sheet in the document with the given text. 1.228 + * Return an actor for it. 1.229 + * 1.230 + * @param {object} request 1.231 + * Debugging protocol request object, with 'text property' 1.232 + * @return {object} 1.233 + * Object with 'styelSheet' property for form on new actor. 1.234 + */ 1.235 + newStyleSheet: method(function(text) { 1.236 + let parent = this.document.documentElement; 1.237 + let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); 1.238 + style.setAttribute("type", "text/css"); 1.239 + 1.240 + if (text) { 1.241 + style.appendChild(this.document.createTextNode(text)); 1.242 + } 1.243 + parent.appendChild(style); 1.244 + 1.245 + let actor = this._createStyleSheetActor(style.sheet); 1.246 + return actor; 1.247 + }, { 1.248 + request: { text: Arg(0, "string") }, 1.249 + response: { styleSheet: RetVal("old-stylesheet") } 1.250 + }) 1.251 +}); 1.252 + 1.253 +/** 1.254 + * The corresponding Front object for the StyleEditorActor. 1.255 + */ 1.256 +let StyleEditorFront = protocol.FrontClass(StyleEditorActor, { 1.257 + initialize: function(client, tabForm) { 1.258 + protocol.Front.prototype.initialize.call(this, client); 1.259 + this.actorID = tabForm.styleEditorActor; 1.260 + 1.261 + client.addActorPool(this); 1.262 + this.manage(this); 1.263 + }, 1.264 + 1.265 + getStyleSheets: function() { 1.266 + let deferred = promise.defer(); 1.267 + 1.268 + events.once(this, "document-load", (styleSheets) => { 1.269 + deferred.resolve(styleSheets); 1.270 + }); 1.271 + this.newDocument(); 1.272 + 1.273 + return deferred.promise; 1.274 + }, 1.275 + 1.276 + addStyleSheet: function(text) { 1.277 + return this.newStyleSheet(text); 1.278 + } 1.279 +}); 1.280 + 1.281 +/** 1.282 + * A StyleSheetActor represents a stylesheet on the server. 1.283 + */ 1.284 +let OldStyleSheetActor = protocol.ActorClass({ 1.285 + typeName: "old-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 + "source-load" : { 1.294 + type: "sourceLoad", 1.295 + source: Arg(0, "string") 1.296 + }, 1.297 + "style-applied" : { 1.298 + type: "styleApplied" 1.299 + } 1.300 + }, 1.301 + 1.302 + toString: function() { 1.303 + return "[OldStyleSheetActor " + this.actorID + "]"; 1.304 + }, 1.305 + 1.306 + /** 1.307 + * Window of target 1.308 + */ 1.309 + get window() this._window || this.parentActor.window, 1.310 + 1.311 + /** 1.312 + * Document of target. 1.313 + */ 1.314 + get document() this.window.document, 1.315 + 1.316 + /** 1.317 + * URL of underlying stylesheet. 1.318 + */ 1.319 + get href() this.rawSheet.href, 1.320 + 1.321 + /** 1.322 + * Retrieve the index (order) of stylesheet in the document. 1.323 + * 1.324 + * @return number 1.325 + */ 1.326 + get styleSheetIndex() 1.327 + { 1.328 + if (this._styleSheetIndex == -1) { 1.329 + for (let i = 0; i < this.document.styleSheets.length; i++) { 1.330 + if (this.document.styleSheets[i] == this.rawSheet) { 1.331 + this._styleSheetIndex = i; 1.332 + break; 1.333 + } 1.334 + } 1.335 + } 1.336 + return this._styleSheetIndex; 1.337 + }, 1.338 + 1.339 + initialize: function(aStyleSheet, aParentActor, aWindow) { 1.340 + protocol.Actor.prototype.initialize.call(this, null); 1.341 + 1.342 + this.rawSheet = aStyleSheet; 1.343 + this.parentActor = aParentActor; 1.344 + this.conn = this.parentActor.conn; 1.345 + 1.346 + this._window = aWindow; 1.347 + 1.348 + // text and index are unknown until source load 1.349 + this.text = null; 1.350 + this._styleSheetIndex = -1; 1.351 + 1.352 + this._transitionRefCount = 0; 1.353 + 1.354 + // if this sheet has an @import, then it's rules are loaded async 1.355 + let ownerNode = this.rawSheet.ownerNode; 1.356 + if (ownerNode) { 1.357 + let onSheetLoaded = function(event) { 1.358 + ownerNode.removeEventListener("load", onSheetLoaded, false); 1.359 + this._notifyPropertyChanged("ruleCount"); 1.360 + }.bind(this); 1.361 + 1.362 + ownerNode.addEventListener("load", onSheetLoaded, false); 1.363 + } 1.364 + }, 1.365 + 1.366 + /** 1.367 + * Get the current state of the actor 1.368 + * 1.369 + * @return {object} 1.370 + * With properties of the underlying stylesheet, plus 'text', 1.371 + * 'styleSheetIndex' and 'parentActor' if it's @imported 1.372 + */ 1.373 + form: function(detail) { 1.374 + if (detail === "actorid") { 1.375 + return this.actorID; 1.376 + } 1.377 + 1.378 + let docHref; 1.379 + if (this.rawSheet.ownerNode) { 1.380 + if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { 1.381 + docHref = this.rawSheet.ownerNode.location.href; 1.382 + } 1.383 + if (this.rawSheet.ownerNode.ownerDocument) { 1.384 + docHref = this.rawSheet.ownerNode.ownerDocument.location.href; 1.385 + } 1.386 + } 1.387 + 1.388 + let form = { 1.389 + actor: this.actorID, // actorID is set when this actor is added to a pool 1.390 + href: this.href, 1.391 + nodeHref: docHref, 1.392 + disabled: this.rawSheet.disabled, 1.393 + title: this.rawSheet.title, 1.394 + system: !CssLogic.isContentStylesheet(this.rawSheet), 1.395 + styleSheetIndex: this.styleSheetIndex 1.396 + } 1.397 + 1.398 + try { 1.399 + form.ruleCount = this.rawSheet.cssRules.length; 1.400 + } 1.401 + catch(e) { 1.402 + // stylesheet had an @import rule that wasn't loaded yet 1.403 + } 1.404 + return form; 1.405 + }, 1.406 + 1.407 + /** 1.408 + * Toggle the disabled property of the style sheet 1.409 + * 1.410 + * @return {object} 1.411 + * 'disabled' - the disabled state after toggling. 1.412 + */ 1.413 + toggleDisabled: method(function() { 1.414 + this.rawSheet.disabled = !this.rawSheet.disabled; 1.415 + this._notifyPropertyChanged("disabled"); 1.416 + 1.417 + return this.rawSheet.disabled; 1.418 + }, { 1.419 + response: { disabled: RetVal("boolean")} 1.420 + }), 1.421 + 1.422 + /** 1.423 + * Send an event notifying that a property of the stylesheet 1.424 + * has changed. 1.425 + * 1.426 + * @param {string} property 1.427 + * Name of the changed property 1.428 + */ 1.429 + _notifyPropertyChanged: function(property) { 1.430 + events.emit(this, "property-change", property, this.form()[property]); 1.431 + }, 1.432 + 1.433 + /** 1.434 + * Fetch the source of the style sheet from its URL. Send a "sourceLoad" 1.435 + * event when it's been fetched. 1.436 + */ 1.437 + fetchSource: method(function() { 1.438 + this._getText().then((content) => { 1.439 + events.emit(this, "source-load", this.text); 1.440 + }); 1.441 + }), 1.442 + 1.443 + /** 1.444 + * Fetch the text for this stylesheet from the cache or network. Return 1.445 + * cached text if it's already been fetched. 1.446 + * 1.447 + * @return {Promise} 1.448 + * Promise that resolves with a string text of the stylesheet. 1.449 + */ 1.450 + _getText: function() { 1.451 + if (this.text) { 1.452 + return promise.resolve(this.text); 1.453 + } 1.454 + 1.455 + if (!this.href) { 1.456 + // this is an inline <style> sheet 1.457 + let content = this.rawSheet.ownerNode.textContent; 1.458 + this.text = content; 1.459 + return promise.resolve(content); 1.460 + } 1.461 + 1.462 + let options = { 1.463 + window: this.window, 1.464 + charset: this._getCSSCharset() 1.465 + }; 1.466 + 1.467 + return fetch(this.href, options).then(({ content }) => { 1.468 + this.text = content; 1.469 + return content; 1.470 + }); 1.471 + }, 1.472 + 1.473 + /** 1.474 + * Get the charset of the stylesheet according to the character set rules 1.475 + * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>. 1.476 + * 1.477 + * @param string channelCharset 1.478 + * Charset of the source string if set by the HTTP channel. 1.479 + */ 1.480 + _getCSSCharset: function(channelCharset) 1.481 + { 1.482 + // StyleSheet's charset can be specified from multiple sources 1.483 + if (channelCharset && channelCharset.length > 0) { 1.484 + // step 1 of syndata.html: charset given in HTTP header. 1.485 + return channelCharset; 1.486 + } 1.487 + 1.488 + let sheet = this.rawSheet; 1.489 + if (sheet) { 1.490 + // Do we have a @charset rule in the stylesheet? 1.491 + // step 2 of syndata.html (without the BOM check). 1.492 + if (sheet.cssRules) { 1.493 + let rules = sheet.cssRules; 1.494 + if (rules.length 1.495 + && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) { 1.496 + return rules.item(0).encoding; 1.497 + } 1.498 + } 1.499 + 1.500 + // step 3: charset attribute of <link> or <style> element, if it exists 1.501 + if (sheet.ownerNode && sheet.ownerNode.getAttribute) { 1.502 + let linkCharset = sheet.ownerNode.getAttribute("charset"); 1.503 + if (linkCharset != null) { 1.504 + return linkCharset; 1.505 + } 1.506 + } 1.507 + 1.508 + // step 4 (1 of 2): charset of referring stylesheet. 1.509 + let parentSheet = sheet.parentStyleSheet; 1.510 + if (parentSheet && parentSheet.cssRules && 1.511 + parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) { 1.512 + return parentSheet.cssRules[0].encoding; 1.513 + } 1.514 + 1.515 + // step 4 (2 of 2): charset of referring document. 1.516 + if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) { 1.517 + return sheet.ownerNode.ownerDocument.characterSet; 1.518 + } 1.519 + } 1.520 + 1.521 + // step 5: default to utf-8. 1.522 + return "UTF-8"; 1.523 + }, 1.524 + 1.525 + /** 1.526 + * Update the style sheet in place with new text. 1.527 + * 1.528 + * @param {object} request 1.529 + * 'text' - new text 1.530 + * 'transition' - whether to do CSS transition for change. 1.531 + */ 1.532 + update: method(function(text, transition) { 1.533 + DOMUtils.parseStyleSheet(this.rawSheet, text); 1.534 + 1.535 + this.text = text; 1.536 + 1.537 + this._notifyPropertyChanged("ruleCount"); 1.538 + 1.539 + if (transition) { 1.540 + this._insertTransistionRule(); 1.541 + } 1.542 + else { 1.543 + this._notifyStyleApplied(); 1.544 + } 1.545 + }, { 1.546 + request: { 1.547 + text: Arg(0, "string"), 1.548 + transition: Arg(1, "boolean") 1.549 + } 1.550 + }), 1.551 + 1.552 + /** 1.553 + * Insert a catch-all transition rule into the document. Set a timeout 1.554 + * to remove the rule after a certain time. 1.555 + */ 1.556 + _insertTransistionRule: function() { 1.557 + // Insert the global transition rule 1.558 + // Use a ref count to make sure we do not add it multiple times.. and remove 1.559 + // it only when all pending StyleEditor-generated transitions ended. 1.560 + if (this._transitionRefCount == 0) { 1.561 + this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length); 1.562 + this.document.documentElement.classList.add(TRANSITION_CLASS); 1.563 + } 1.564 + 1.565 + this._transitionRefCount++; 1.566 + 1.567 + // Set up clean up and commit after transition duration (+10% buffer) 1.568 + // @see _onTransitionEnd 1.569 + this.window.setTimeout(this._onTransitionEnd.bind(this), 1.570 + Math.floor(TRANSITION_DURATION_MS * 1.1)); 1.571 + }, 1.572 + 1.573 + /** 1.574 + * This cleans up class and rule added for transition effect and then 1.575 + * notifies that the style has been applied. 1.576 + */ 1.577 + _onTransitionEnd: function() 1.578 + { 1.579 + if (--this._transitionRefCount == 0) { 1.580 + this.document.documentElement.classList.remove(TRANSITION_CLASS); 1.581 + this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1); 1.582 + } 1.583 + 1.584 + events.emit(this, "style-applied"); 1.585 + } 1.586 +}) 1.587 + 1.588 +/** 1.589 + * StyleSheetFront is the client-side counterpart to a StyleSheetActor. 1.590 + */ 1.591 +var OldStyleSheetFront = protocol.FrontClass(OldStyleSheetActor, { 1.592 + initialize: function(conn, form, ctx, detail) { 1.593 + protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail); 1.594 + 1.595 + this._onPropertyChange = this._onPropertyChange.bind(this); 1.596 + events.on(this, "property-change", this._onPropertyChange); 1.597 + }, 1.598 + 1.599 + destroy: function() { 1.600 + events.off(this, "property-change", this._onPropertyChange); 1.601 + 1.602 + protocol.Front.prototype.destroy.call(this); 1.603 + }, 1.604 + 1.605 + _onPropertyChange: function(property, value) { 1.606 + this._form[property] = value; 1.607 + }, 1.608 + 1.609 + form: function(form, detail) { 1.610 + if (detail === "actorid") { 1.611 + this.actorID = form; 1.612 + return; 1.613 + } 1.614 + this.actorID = form.actor; 1.615 + this._form = form; 1.616 + }, 1.617 + 1.618 + getText: function() { 1.619 + let deferred = promise.defer(); 1.620 + 1.621 + events.once(this, "source-load", (source) => { 1.622 + let longStr = new ShortLongString(source); 1.623 + deferred.resolve(longStr); 1.624 + }); 1.625 + this.fetchSource(); 1.626 + 1.627 + return deferred.promise; 1.628 + }, 1.629 + 1.630 + getOriginalSources: function() { 1.631 + return promise.resolve([]); 1.632 + }, 1.633 + 1.634 + get href() this._form.href, 1.635 + get nodeHref() this._form.nodeHref, 1.636 + get disabled() !!this._form.disabled, 1.637 + get title() this._form.title, 1.638 + get isSystem() this._form.system, 1.639 + get styleSheetIndex() this._form.styleSheetIndex, 1.640 + get ruleCount() this._form.ruleCount 1.641 +}); 1.642 + 1.643 +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { 1.644 + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); 1.645 +}); 1.646 + 1.647 +exports.StyleEditorActor = StyleEditorActor; 1.648 +exports.StyleEditorFront = StyleEditorFront; 1.649 + 1.650 +exports.OldStyleSheetActor = OldStyleSheetActor; 1.651 +exports.OldStyleSheetFront = OldStyleSheetFront; 1.652 + 1.653 + 1.654 +/** 1.655 + * Performs a request to load the desired URL and returns a promise. 1.656 + * 1.657 + * @param aURL String 1.658 + * The URL we will request. 1.659 + * @returns Promise 1.660 + * A promise of the document at that URL, as a string. 1.661 + */ 1.662 +function fetch(aURL, aOptions={ loadFromCache: true, window: null, 1.663 + charset: null}) { 1.664 + let deferred = promise.defer(); 1.665 + let scheme; 1.666 + let url = aURL.split(" -> ").pop(); 1.667 + let charset; 1.668 + let contentType; 1.669 + 1.670 + try { 1.671 + scheme = Services.io.extractScheme(url); 1.672 + } catch (e) { 1.673 + // In the xpcshell tests, the script url is the absolute path of the test 1.674 + // file, which will make a malformed URI error be thrown. Add the file 1.675 + // scheme prefix ourselves. 1.676 + url = "file://" + url; 1.677 + scheme = Services.io.extractScheme(url); 1.678 + } 1.679 + 1.680 + switch (scheme) { 1.681 + case "file": 1.682 + case "chrome": 1.683 + case "resource": 1.684 + try { 1.685 + NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) { 1.686 + if (!components.isSuccessCode(aStatus)) { 1.687 + deferred.reject(new Error("Request failed with status code = " 1.688 + + aStatus 1.689 + + " after NetUtil.asyncFetch for url = " 1.690 + + url)); 1.691 + return; 1.692 + } 1.693 + 1.694 + let source = NetUtil.readInputStreamToString(aStream, aStream.available()); 1.695 + contentType = aRequest.contentType; 1.696 + deferred.resolve(source); 1.697 + aStream.close(); 1.698 + }); 1.699 + } catch (ex) { 1.700 + deferred.reject(ex); 1.701 + } 1.702 + break; 1.703 + 1.704 + default: 1.705 + let channel; 1.706 + try { 1.707 + channel = Services.io.newChannel(url, null, null); 1.708 + } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") { 1.709 + // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but 1.710 + // newChannel won't be able to handle it. 1.711 + url = "file:///" + url; 1.712 + channel = Services.io.newChannel(url, null, null); 1.713 + } 1.714 + let chunks = []; 1.715 + let streamListener = { 1.716 + onStartRequest: function(aRequest, aContext, aStatusCode) { 1.717 + if (!components.isSuccessCode(aStatusCode)) { 1.718 + deferred.reject(new Error("Request failed with status code = " 1.719 + + aStatusCode 1.720 + + " in onStartRequest handler for url = " 1.721 + + url)); 1.722 + } 1.723 + }, 1.724 + onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { 1.725 + chunks.push(NetUtil.readInputStreamToString(aStream, aCount)); 1.726 + }, 1.727 + onStopRequest: function(aRequest, aContext, aStatusCode) { 1.728 + if (!components.isSuccessCode(aStatusCode)) { 1.729 + deferred.reject(new Error("Request failed with status code = " 1.730 + + aStatusCode 1.731 + + " in onStopRequest handler for url = " 1.732 + + url)); 1.733 + return; 1.734 + } 1.735 + 1.736 + charset = channel.contentCharset || charset; 1.737 + contentType = channel.contentType; 1.738 + deferred.resolve(chunks.join("")); 1.739 + } 1.740 + }; 1.741 + 1.742 + if (aOptions.window) { 1.743 + // respect private browsing 1.744 + channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor) 1.745 + .getInterface(Ci.nsIWebNavigation) 1.746 + .QueryInterface(Ci.nsIDocumentLoader) 1.747 + .loadGroup; 1.748 + } 1.749 + channel.loadFlags = aOptions.loadFromCache 1.750 + ? channel.LOAD_FROM_CACHE 1.751 + : channel.LOAD_BYPASS_CACHE; 1.752 + channel.asyncOpen(streamListener, null); 1.753 + break; 1.754 + } 1.755 + 1.756 + return deferred.promise.then(source => { 1.757 + return { 1.758 + content: convertToUnicode(source, charset), 1.759 + contentType: contentType 1.760 + }; 1.761 + }); 1.762 +} 1.763 + 1.764 +/** 1.765 + * Convert a given string, encoded in a given character set, to unicode. 1.766 + * 1.767 + * @param string aString 1.768 + * A string. 1.769 + * @param string aCharset 1.770 + * A character set. 1.771 + */ 1.772 +function convertToUnicode(aString, aCharset=null) { 1.773 + // Decoding primitives. 1.774 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.775 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.776 + try { 1.777 + converter.charset = aCharset || "UTF-8"; 1.778 + return converter.ConvertToUnicode(aString); 1.779 + } catch(e) { 1.780 + return aString; 1.781 + } 1.782 +} 1.783 + 1.784 +/** 1.785 + * Normalize multiple relative paths towards the base paths on the right. 1.786 + */ 1.787 +function normalize(...aURLs) { 1.788 + let base = Services.io.newURI(aURLs.pop(), null, null); 1.789 + let url; 1.790 + while ((url = aURLs.pop())) { 1.791 + base = Services.io.newURI(url, null, base); 1.792 + } 1.793 + return base.spec; 1.794 +} 1.795 + 1.796 +function dirname(aPath) { 1.797 + return Services.io.newURI( 1.798 + ".", null, Services.io.newURI(aPath, null, null)).spec; 1.799 +}