toolkit/devtools/server/actors/stylesheets.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 let { components, Cc, Ci, Cu } = require("chrome");
     8 let Services = require("Services");
    10 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    11 Cu.import("resource://gre/modules/NetUtil.jsm");
    12 Cu.import("resource://gre/modules/FileUtils.jsm");
    13 Cu.import("resource://gre/modules/devtools/SourceMap.jsm");
    14 Cu.import("resource://gre/modules/Task.jsm");
    16 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    17 const events = require("sdk/event/core");
    18 const protocol = require("devtools/server/protocol");
    19 const {Arg, Option, method, RetVal, types} = protocol;
    20 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
    22 loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
    24 let TRANSITION_CLASS = "moz-styleeditor-transitioning";
    25 let TRANSITION_DURATION_MS = 500;
    26 let TRANSITION_BUFFER_MS = 1000;
    27 let TRANSITION_RULE = "\
    28 :root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\
    29 transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
    30 transition-delay: 0ms !important;\
    31 transition-timing-function: ease-out !important;\
    32 transition-property: all !important;\
    33 }";
    35 let LOAD_ERROR = "error-load";
    37 exports.register = function(handle) {
    38   handle.addTabActor(StyleSheetsActor, "styleSheetsActor");
    39   handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor");
    40 };
    42 exports.unregister = function(handle) {
    43   handle.removeTabActor(StyleSheetsActor);
    44   handle.removeGlobalActor(StyleSheetsActor);
    45 };
    47 types.addActorType("stylesheet");
    48 types.addActorType("originalsource");
    50 /**
    51  * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the
    52  * stylesheets of a document.
    53  */
    54 let StyleSheetsActor = protocol.ActorClass({
    55   typeName: "stylesheets",
    57   /**
    58    * The window we work with, taken from the parent actor.
    59    */
    60   get window() this.parentActor.window,
    62   /**
    63    * The current content document of the window we work with.
    64    */
    65   get document() this.window.document,
    67   form: function()
    68   {
    69     return { actor: this.actorID };
    70   },
    72   initialize: function (conn, tabActor) {
    73     protocol.Actor.prototype.initialize.call(this, null);
    75     this.parentActor = tabActor;
    77     // keep a map of sheets-to-actors so we don't create two actors for one sheet
    78     this._sheets = new Map();
    79   },
    81   /**
    82    * Destroy the current StyleSheetsActor instance.
    83    */
    84   destroy: function()
    85   {
    86     this._sheets.clear();
    87   },
    89   /**
    90    * Protocol method for getting a list of StyleSheetActors representing
    91    * all the style sheets in this document.
    92    */
    93   getStyleSheets: method(function() {
    94     let deferred = promise.defer();
    96     let window = this.window;
    97     var domReady = () => {
    98       window.removeEventListener("DOMContentLoaded", domReady, true);
    99       this._addAllStyleSheets().then(deferred.resolve, Cu.reportError);
   100     };
   102     if (window.document.readyState === "loading") {
   103       window.addEventListener("DOMContentLoaded", domReady, true);
   104     } else {
   105       domReady();
   106     }
   108     return deferred.promise;
   109   }, {
   110     request: {},
   111     response: { styleSheets: RetVal("array:stylesheet") }
   112   }),
   114   /**
   115    * Add all the stylesheets in this document and its subframes.
   116    * Assumes the document is loaded.
   117    *
   118    * @return {Promise}
   119    *         Promise that resolves with an array of StyleSheetActors
   120    */
   121   _addAllStyleSheets: function() {
   122     return Task.spawn(function() {
   123       let documents = [this.document];
   124       let actors = [];
   126       for (let doc of documents) {
   127         let sheets = yield this._addStyleSheets(doc.styleSheets);
   128         actors = actors.concat(sheets);
   130         // Recursively handle style sheets of the documents in iframes.
   131         for (let iframe of doc.getElementsByTagName("iframe")) {
   132           if (iframe.contentDocument) {
   133             // Sometimes, iframes don't have any document, like the
   134             // one that are over deeply nested (bug 285395)
   135             documents.push(iframe.contentDocument);
   136           }
   137         }
   138       }
   139       throw new Task.Result(actors);
   140     }.bind(this));
   141   },
   143   /**
   144    * Add all the stylesheets to the map and create an actor for each one
   145    * if not already created.
   146    *
   147    * @param {[DOMStyleSheet]} styleSheets
   148    *        Stylesheets to add
   149    *
   150    * @return {Promise}
   151    *         Promise that resolves to an array of StyleSheetActors
   152    */
   153   _addStyleSheets: function(styleSheets)
   154   {
   155     return Task.spawn(function() {
   156       let actors = [];
   157       for (let i = 0; i < styleSheets.length; i++) {
   158         let actor = this._createStyleSheetActor(styleSheets[i]);
   159         actors.push(actor);
   161         // Get all sheets, including imported ones
   162         let imports = yield this._getImported(actor);
   163         actors = actors.concat(imports);
   164       }
   165       throw new Task.Result(actors);
   166     }.bind(this));
   167   },
   169   /**
   170    * Get all the stylesheets @imported from a stylesheet.
   171    *
   172    * @param  {DOMStyleSheet} styleSheet
   173    *         Style sheet to search
   174    * @return {Promise}
   175    *         A promise that resolves with an array of StyleSheetActors
   176    */
   177   _getImported: function(styleSheet) {
   178     return Task.spawn(function() {
   179       let rules = yield styleSheet.getCSSRules();
   180       let imported = [];
   182       for (let i = 0; i < rules.length; i++) {
   183         let rule = rules[i];
   184         if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
   185           // Associated styleSheet may be null if it has already been seen due
   186           // to duplicate @imports for the same URL.
   187           if (!rule.styleSheet) {
   188             continue;
   189           }
   190           let actor = this._createStyleSheetActor(rule.styleSheet);
   191           imported.push(actor);
   193           // recurse imports in this stylesheet as well
   194           let children = yield this._getImported(actor);
   195           imported = imported.concat(children);
   196         }
   197         else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
   198           // @import rules must precede all others except @charset
   199           break;
   200         }
   201       }
   203       throw new Task.Result(imported);
   204     }.bind(this));
   205   },
   207   /**
   208    * Create a new actor for a style sheet, if it hasn't already been created.
   209    *
   210    * @param  {DOMStyleSheet} styleSheet
   211    *         The style sheet to create an actor for.
   212    * @return {StyleSheetActor}
   213    *         The actor for this style sheet
   214    */
   215   _createStyleSheetActor: function(styleSheet)
   216   {
   217     if (this._sheets.has(styleSheet)) {
   218       return this._sheets.get(styleSheet);
   219     }
   220     let actor = new StyleSheetActor(styleSheet, this);
   222     this.manage(actor);
   223     this._sheets.set(styleSheet, actor);
   225     return actor;
   226   },
   228   /**
   229    * Clear all the current stylesheet actors in map.
   230    */
   231   _clearStyleSheetActors: function() {
   232     for (let actor in this._sheets) {
   233       this.unmanage(this._sheets[actor]);
   234     }
   235     this._sheets.clear();
   236   },
   238   /**
   239    * Create a new style sheet in the document with the given text.
   240    * Return an actor for it.
   241    *
   242    * @param  {object} request
   243    *         Debugging protocol request object, with 'text property'
   244    * @return {object}
   245    *         Object with 'styelSheet' property for form on new actor.
   246    */
   247   addStyleSheet: method(function(text) {
   248     let parent = this.document.documentElement;
   249     let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
   250     style.setAttribute("type", "text/css");
   252     if (text) {
   253       style.appendChild(this.document.createTextNode(text));
   254     }
   255     parent.appendChild(style);
   257     let actor = this._createStyleSheetActor(style.sheet);
   258     return actor;
   259   }, {
   260     request: { text: Arg(0, "string") },
   261     response: { styleSheet: RetVal("stylesheet") }
   262   })
   263 });
   265 /**
   266  * The corresponding Front object for the StyleSheetsActor.
   267  */
   268 let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, {
   269   initialize: function(client, tabForm) {
   270     protocol.Front.prototype.initialize.call(this, client);
   271     this.actorID = tabForm.styleSheetsActor;
   273     client.addActorPool(this);
   274     this.manage(this);
   275   }
   276 });
   278 /**
   279  * A StyleSheetActor represents a stylesheet on the server.
   280  */
   281 let StyleSheetActor = protocol.ActorClass({
   282   typeName: "stylesheet",
   284   events: {
   285     "property-change" : {
   286       type: "propertyChange",
   287       property: Arg(0, "string"),
   288       value: Arg(1, "json")
   289     },
   290     "style-applied" : {
   291       type: "styleApplied"
   292     }
   293   },
   295   /* List of original sources that generated this stylesheet */
   296   _originalSources: null,
   298   toString: function() {
   299     return "[StyleSheetActor " + this.actorID + "]";
   300   },
   302   /**
   303    * Window of target
   304    */
   305   get window() this._window || this.parentActor.window,
   307   /**
   308    * Document of target.
   309    */
   310   get document() this.window.document,
   312   /**
   313    * URL of underlying stylesheet.
   314    */
   315   get href() this.rawSheet.href,
   317   /**
   318    * Retrieve the index (order) of stylesheet in the document.
   319    *
   320    * @return number
   321    */
   322   get styleSheetIndex()
   323   {
   324     if (this._styleSheetIndex == -1) {
   325       for (let i = 0; i < this.document.styleSheets.length; i++) {
   326         if (this.document.styleSheets[i] == this.rawSheet) {
   327           this._styleSheetIndex = i;
   328           break;
   329         }
   330       }
   331     }
   332     return this._styleSheetIndex;
   333   },
   335   initialize: function(aStyleSheet, aParentActor, aWindow) {
   336     protocol.Actor.prototype.initialize.call(this, null);
   338     this.rawSheet = aStyleSheet;
   339     this.parentActor = aParentActor;
   340     this.conn = this.parentActor.conn;
   342     this._window = aWindow;
   344     // text and index are unknown until source load
   345     this.text = null;
   346     this._styleSheetIndex = -1;
   348     this._transitionRefCount = 0;
   349   },
   351   /**
   352    * Get the raw stylesheet's cssRules once the sheet has been loaded.
   353    *
   354    * @return {Promise}
   355    *         Promise that resolves with a CSSRuleList
   356    */
   357   getCSSRules: function() {
   358     let rules;
   359     try {
   360       rules = this.rawSheet.cssRules;
   361     }
   362     catch (e) {
   363       // sheet isn't loaded yet
   364     }
   366     if (rules) {
   367       return promise.resolve(rules);
   368     }
   370     let ownerNode = this.rawSheet.ownerNode;
   371     if (!ownerNode) {
   372       return promise.resolve([]);
   373     }
   375     if (this._cssRules) {
   376       return this._cssRules;
   377     }
   379     let deferred = promise.defer();
   381     let onSheetLoaded = function(event) {
   382       ownerNode.removeEventListener("load", onSheetLoaded, false);
   384       deferred.resolve(this.rawSheet.cssRules);
   385     }.bind(this);
   387     ownerNode.addEventListener("load", onSheetLoaded, false);
   389     // cache so we don't add many listeners if this is called multiple times.
   390     this._cssRules = deferred.promise;
   392     return this._cssRules;
   393   },
   395   /**
   396    * Get the current state of the actor
   397    *
   398    * @return {object}
   399    *         With properties of the underlying stylesheet, plus 'text',
   400    *        'styleSheetIndex' and 'parentActor' if it's @imported
   401    */
   402   form: function(detail) {
   403     if (detail === "actorid") {
   404       return this.actorID;
   405     }
   407     let docHref;
   408     let ownerNode = this.rawSheet.ownerNode;
   409     if (ownerNode) {
   410       if (ownerNode instanceof Ci.nsIDOMHTMLDocument) {
   411         docHref = ownerNode.location.href;
   412       }
   413       else if (ownerNode.ownerDocument && ownerNode.ownerDocument.location) {
   414         docHref = ownerNode.ownerDocument.location.href;
   415       }
   416     }
   418     let form = {
   419       actor: this.actorID,  // actorID is set when this actor is added to a pool
   420       href: this.href,
   421       nodeHref: docHref,
   422       disabled: this.rawSheet.disabled,
   423       title: this.rawSheet.title,
   424       system: !CssLogic.isContentStylesheet(this.rawSheet),
   425       styleSheetIndex: this.styleSheetIndex
   426     }
   428     try {
   429       form.ruleCount = this.rawSheet.cssRules.length;
   430     }
   431     catch(e) {
   432       // stylesheet had an @import rule that wasn't loaded yet
   433       this.getCSSRules().then(() => {
   434         this._notifyPropertyChanged("ruleCount");
   435       });
   436     }
   437     return form;
   438   },
   440   /**
   441    * Toggle the disabled property of the style sheet
   442    *
   443    * @return {object}
   444    *         'disabled' - the disabled state after toggling.
   445    */
   446   toggleDisabled: method(function() {
   447     this.rawSheet.disabled = !this.rawSheet.disabled;
   448     this._notifyPropertyChanged("disabled");
   450     return this.rawSheet.disabled;
   451   }, {
   452     response: { disabled: RetVal("boolean")}
   453   }),
   455   /**
   456    * Send an event notifying that a property of the stylesheet
   457    * has changed.
   458    *
   459    * @param  {string} property
   460    *         Name of the changed property
   461    */
   462   _notifyPropertyChanged: function(property) {
   463     events.emit(this, "property-change", property, this.form()[property]);
   464   },
   466   /**
   467    * Protocol method to get the text of this stylesheet.
   468    */
   469   getText: method(function() {
   470     return this._getText().then((text) => {
   471       return new LongStringActor(this.conn, text || "");
   472     });
   473   }, {
   474     response: {
   475       text: RetVal("longstring")
   476     }
   477   }),
   479   /**
   480    * Fetch the text for this stylesheet from the cache or network. Return
   481    * cached text if it's already been fetched.
   482    *
   483    * @return {Promise}
   484    *         Promise that resolves with a string text of the stylesheet.
   485    */
   486   _getText: function() {
   487     if (this.text) {
   488       return promise.resolve(this.text);
   489     }
   491     if (!this.href) {
   492       // this is an inline <style> sheet
   493       let content = this.rawSheet.ownerNode.textContent;
   494       this.text = content;
   495       return promise.resolve(content);
   496     }
   498     let options = {
   499       window: this.window,
   500       charset: this._getCSSCharset()
   501     };
   503     return fetch(this.href, options).then(({ content }) => {
   504       this.text = content;
   505       return content;
   506     });
   507   },
   509   /**
   510    * Protocol method to get the original source (actors) for this
   511    * stylesheet if it has uses source maps.
   512    */
   513   getOriginalSources: method(function() {
   514     if (this._originalSources) {
   515       return promise.resolve(this._originalSources);
   516     }
   517     return this._fetchOriginalSources();
   518   }, {
   519     request: {},
   520     response: {
   521       originalSources: RetVal("nullable:array:originalsource")
   522     }
   523   }),
   525   /**
   526    * Fetch the original sources (actors) for this style sheet using its
   527    * source map. If they've already been fetched, returns cached array.
   528    *
   529    * @return {Promise}
   530    *         Promise that resolves with an array of OriginalSourceActors
   531    */
   532   _fetchOriginalSources: function() {
   533     this._clearOriginalSources();
   534     this._originalSources = [];
   536     return this.getSourceMap().then((sourceMap) => {
   537       if (!sourceMap) {
   538         return null;
   539       }
   540       for (let url of sourceMap.sources) {
   541         let actor = new OriginalSourceActor(url, sourceMap, this);
   543         this.manage(actor);
   544         this._originalSources.push(actor);
   545       }
   546       return this._originalSources;
   547     })
   548   },
   550   /**
   551    * Get the SourceMapConsumer for this stylesheet's source map, if
   552    * it exists. Saves the consumer for later queries.
   553    *
   554    * @return {Promise}
   555    *         A promise that resolves with a SourceMapConsumer, or null.
   556    */
   557   getSourceMap: function() {
   558     if (this._sourceMap) {
   559       return this._sourceMap;
   560     }
   561     return this._fetchSourceMap();
   562   },
   564   /**
   565    * Fetch the source map for this stylesheet.
   566    *
   567    * @return {Promise}
   568    *         A promise that resolves with a SourceMapConsumer, or null.
   569    */
   570   _fetchSourceMap: function() {
   571     let deferred = promise.defer();
   573     this._getText().then((content) => {
   574       let url = this._extractSourceMapUrl(content);
   575       if (!url) {
   576         // no source map for this stylesheet
   577         deferred.resolve(null);
   578         return;
   579       };
   581       url = normalize(url, this.href);
   583       let map = fetch(url, { loadFromCache: false, window: this.window })
   584         .then(({content}) => {
   585           let map = new SourceMapConsumer(content);
   586           this._setSourceMapRoot(map, url, this.href);
   587           this._sourceMap = promise.resolve(map);
   589           deferred.resolve(map);
   590           return map;
   591         }, deferred.reject);
   593       this._sourceMap = map;
   594     }, deferred.reject);
   596     return deferred.promise;
   597   },
   599   /**
   600    * Clear and unmanage the original source actors for this stylesheet.
   601    */
   602   _clearOriginalSources: function() {
   603     for (actor in this._originalSources) {
   604       this.unmanage(actor);
   605     }
   606     this._originalSources = null;
   607   },
   609   /**
   610    * Sets the source map's sourceRoot to be relative to the source map url.
   611    */
   612   _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) {
   613     const base = dirname(
   614       aAbsSourceMapURL.startsWith("data:")
   615         ? aScriptURL
   616         : aAbsSourceMapURL);
   617     aSourceMap.sourceRoot = aSourceMap.sourceRoot
   618       ? normalize(aSourceMap.sourceRoot, base)
   619       : base;
   620   },
   622   /**
   623    * Get the source map url specified in the text of a stylesheet.
   624    *
   625    * @param  {string} content
   626    *         The text of the style sheet.
   627    * @return {string}
   628    *         Url of source map.
   629    */
   630   _extractSourceMapUrl: function(content) {
   631     var matches = /sourceMappingURL\=([^\s\*]*)/.exec(content);
   632     if (matches) {
   633       return matches[1];
   634     }
   635     return null;
   636   },
   638   /**
   639    * Protocol method that gets the location in the original source of a
   640    * line, column pair in this stylesheet, if its source mapped, otherwise
   641    * a promise of the same location.
   642    */
   643   getOriginalLocation: method(function(line, column) {
   644     return this.getSourceMap().then((sourceMap) => {
   645       if (sourceMap) {
   646         return sourceMap.originalPositionFor({ line: line, column: column });
   647       }
   648       return {
   649         source: this.href,
   650         line: line,
   651         column: column
   652       }
   653     });
   654   }, {
   655     request: {
   656       line: Arg(0, "number"),
   657       column: Arg(1, "number")
   658     },
   659     response: RetVal(types.addDictType("originallocationresponse", {
   660       source: "string",
   661       line: "number",
   662       column: "number"
   663     }))
   664   }),
   666   /**
   667    * Get the charset of the stylesheet according to the character set rules
   668    * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>.
   669    *
   670    * @param string channelCharset
   671    *        Charset of the source string if set by the HTTP channel.
   672    */
   673   _getCSSCharset: function(channelCharset)
   674   {
   675     // StyleSheet's charset can be specified from multiple sources
   676     if (channelCharset && channelCharset.length > 0) {
   677       // step 1 of syndata.html: charset given in HTTP header.
   678       return channelCharset;
   679     }
   681     let sheet = this.rawSheet;
   682     if (sheet) {
   683       // Do we have a @charset rule in the stylesheet?
   684       // step 2 of syndata.html (without the BOM check).
   685       if (sheet.cssRules) {
   686         let rules = sheet.cssRules;
   687         if (rules.length
   688             && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
   689           return rules.item(0).encoding;
   690         }
   691       }
   693       // step 3: charset attribute of <link> or <style> element, if it exists
   694       if (sheet.ownerNode && sheet.ownerNode.getAttribute) {
   695         let linkCharset = sheet.ownerNode.getAttribute("charset");
   696         if (linkCharset != null) {
   697           return linkCharset;
   698         }
   699       }
   701       // step 4 (1 of 2): charset of referring stylesheet.
   702       let parentSheet = sheet.parentStyleSheet;
   703       if (parentSheet && parentSheet.cssRules &&
   704           parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
   705         return parentSheet.cssRules[0].encoding;
   706       }
   708       // step 4 (2 of 2): charset of referring document.
   709       if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) {
   710         return sheet.ownerNode.ownerDocument.characterSet;
   711       }
   712     }
   714     // step 5: default to utf-8.
   715     return "UTF-8";
   716   },
   718   /**
   719    * Update the style sheet in place with new text.
   720    *
   721    * @param  {object} request
   722    *         'text' - new text
   723    *         'transition' - whether to do CSS transition for change.
   724    */
   725   update: method(function(text, transition) {
   726     DOMUtils.parseStyleSheet(this.rawSheet, text);
   728     this.text = text;
   730     this._notifyPropertyChanged("ruleCount");
   732     if (transition) {
   733       this._insertTransistionRule();
   734     }
   735     else {
   736       this._notifyStyleApplied();
   737     }
   738   }, {
   739     request: {
   740       text: Arg(0, "string"),
   741       transition: Arg(1, "boolean")
   742     }
   743   }),
   745   /**
   746    * Insert a catch-all transition rule into the document. Set a timeout
   747    * to remove the rule after a certain time.
   748    */
   749   _insertTransistionRule: function() {
   750     // Insert the global transition rule
   751     // Use a ref count to make sure we do not add it multiple times.. and remove
   752     // it only when all pending StyleSheets-generated transitions ended.
   753     if (this._transitionRefCount == 0) {
   754       this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length);
   755       this.document.documentElement.classList.add(TRANSITION_CLASS);
   756     }
   758     this._transitionRefCount++;
   760     // Set up clean up and commit after transition duration (+buffer)
   761     // @see _onTransitionEnd
   762     this.window.setTimeout(this._onTransitionEnd.bind(this),
   763                            TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS);
   764   },
   766   /**
   767    * This cleans up class and rule added for transition effect and then
   768    * notifies that the style has been applied.
   769    */
   770   _onTransitionEnd: function()
   771   {
   772     if (--this._transitionRefCount == 0) {
   773       this.document.documentElement.classList.remove(TRANSITION_CLASS);
   774       this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1);
   775     }
   777     events.emit(this, "style-applied");
   778   }
   779 })
   781 /**
   782  * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
   783  */
   784 var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
   785   initialize: function(conn, form) {
   786     protocol.Front.prototype.initialize.call(this, conn, form);
   788     this._onPropertyChange = this._onPropertyChange.bind(this);
   789     events.on(this, "property-change", this._onPropertyChange);
   790   },
   792   destroy: function() {
   793     events.off(this, "property-change", this._onPropertyChange);
   795     protocol.Front.prototype.destroy.call(this);
   796   },
   798   _onPropertyChange: function(property, value) {
   799     this._form[property] = value;
   800   },
   802   form: function(form, detail) {
   803     if (detail === "actorid") {
   804       this.actorID = form;
   805       return;
   806     }
   807     this.actorID = form.actor;
   808     this._form = form;
   809   },
   811   get href() this._form.href,
   812   get nodeHref() this._form.nodeHref,
   813   get disabled() !!this._form.disabled,
   814   get title() this._form.title,
   815   get isSystem() this._form.system,
   816   get styleSheetIndex() this._form.styleSheetIndex,
   817   get ruleCount() this._form.ruleCount
   818 });
   820 /**
   821  * Actor representing an original source of a style sheet that was specified
   822  * in a source map.
   823  */
   824 let OriginalSourceActor = protocol.ActorClass({
   825   typeName: "originalsource",
   827   initialize: function(aUrl, aSourceMap, aParentActor) {
   828     protocol.Actor.prototype.initialize.call(this, null);
   830     this.url = aUrl;
   831     this.sourceMap = aSourceMap;
   832     this.parentActor = aParentActor;
   833     this.conn = this.parentActor.conn;
   835     this.text = null;
   836   },
   838   form: function() {
   839     return {
   840       actor: this.actorID, // actorID is set when it's added to a pool
   841       url: this.url,
   842       relatedStyleSheet: this.parentActor.form()
   843     };
   844   },
   846   _getText: function() {
   847     if (this.text) {
   848       return promise.resolve(this.text);
   849     }
   850     let content = this.sourceMap.sourceContentFor(this.url);
   851     if (content) {
   852       this.text = content;
   853       return promise.resolve(content);
   854     }
   855     return fetch(this.url, { window: this.window }).then(({content}) => {
   856       this.text = content;
   857       return content;
   858     });
   859   },
   861   /**
   862    * Protocol method to get the text of this source.
   863    */
   864   getText: method(function() {
   865     return this._getText().then((text) => {
   866       return new LongStringActor(this.conn, text || "");
   867     });
   868   }, {
   869     response: {
   870       text: RetVal("longstring")
   871     }
   872   })
   873 })
   875 /**
   876  * The client-side counterpart for an OriginalSourceActor.
   877  */
   878 let OriginalSourceFront = protocol.FrontClass(OriginalSourceActor, {
   879   initialize: function(client, form) {
   880     protocol.Front.prototype.initialize.call(this, client, form);
   882     this.isOriginalSource = true;
   883   },
   885   form: function(form, detail) {
   886     if (detail === "actorid") {
   887       this.actorID = form;
   888       return;
   889     }
   890     this.actorID = form.actor;
   891     this._form = form;
   892   },
   894   get href() this._form.url,
   895   get url() this._form.url
   896 });
   899 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   900   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
   901 });
   903 exports.StyleSheetsActor = StyleSheetsActor;
   904 exports.StyleSheetsFront = StyleSheetsFront;
   906 exports.StyleSheetActor = StyleSheetActor;
   907 exports.StyleSheetFront = StyleSheetFront;
   910 /**
   911  * Performs a request to load the desired URL and returns a promise.
   912  *
   913  * @param aURL String
   914  *        The URL we will request.
   915  * @returns Promise
   916  *        A promise of the document at that URL, as a string.
   917  */
   918 function fetch(aURL, aOptions={ loadFromCache: true, window: null,
   919                                 charset: null}) {
   920   let deferred = promise.defer();
   921   let scheme;
   922   let url = aURL.split(" -> ").pop();
   923   let charset;
   924   let contentType;
   926   try {
   927     scheme = Services.io.extractScheme(url);
   928   } catch (e) {
   929     // In the xpcshell tests, the script url is the absolute path of the test
   930     // file, which will make a malformed URI error be thrown. Add the file
   931     // scheme prefix ourselves.
   932     url = "file://" + url;
   933     scheme = Services.io.extractScheme(url);
   934   }
   936   switch (scheme) {
   937     case "file":
   938     case "chrome":
   939     case "resource":
   940       try {
   941         NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
   942           if (!components.isSuccessCode(aStatus)) {
   943             deferred.reject(new Error("Request failed with status code = "
   944                                       + aStatus
   945                                       + " after NetUtil.asyncFetch for url = "
   946                                       + url));
   947             return;
   948           }
   950           let source = NetUtil.readInputStreamToString(aStream, aStream.available());
   951           contentType = aRequest.contentType;
   952           deferred.resolve(source);
   953           aStream.close();
   954         });
   955       } catch (ex) {
   956         deferred.reject(ex);
   957       }
   958       break;
   960     default:
   961       let channel;
   962       try {
   963         channel = Services.io.newChannel(url, null, null);
   964       } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
   965         // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
   966         // newChannel won't be able to handle it.
   967         url = "file:///" + url;
   968         channel = Services.io.newChannel(url, null, null);
   969       }
   970       let chunks = [];
   971       let streamListener = {
   972         onStartRequest: function(aRequest, aContext, aStatusCode) {
   973           if (!components.isSuccessCode(aStatusCode)) {
   974             deferred.reject(new Error("Request failed with status code = "
   975                                       + aStatusCode
   976                                       + " in onStartRequest handler for url = "
   977                                       + url));
   978           }
   979         },
   980         onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
   981           chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
   982         },
   983         onStopRequest: function(aRequest, aContext, aStatusCode) {
   984           if (!components.isSuccessCode(aStatusCode)) {
   985             deferred.reject(new Error("Request failed with status code = "
   986                                       + aStatusCode
   987                                       + " in onStopRequest handler for url = "
   988                                       + url));
   989             return;
   990           }
   992           charset = channel.contentCharset || charset;
   993           contentType = channel.contentType;
   994           deferred.resolve(chunks.join(""));
   995         }
   996       };
   998       if (aOptions.window) {
   999         // respect private browsing
  1000         channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor)
  1001                               .getInterface(Ci.nsIWebNavigation)
  1002                               .QueryInterface(Ci.nsIDocumentLoader)
  1003                               .loadGroup;
  1005       channel.loadFlags = aOptions.loadFromCache
  1006         ? channel.LOAD_FROM_CACHE
  1007         : channel.LOAD_BYPASS_CACHE;
  1008       channel.asyncOpen(streamListener, null);
  1009       break;
  1012   return deferred.promise.then(source => {
  1013     return {
  1014       content: convertToUnicode(source, charset),
  1015       contentType: contentType
  1016     };
  1017   });
  1020 /**
  1021  * Convert a given string, encoded in a given character set, to unicode.
  1023  * @param string aString
  1024  *        A string.
  1025  * @param string aCharset
  1026  *        A character set.
  1027  */
  1028 function convertToUnicode(aString, aCharset=null) {
  1029   // Decoding primitives.
  1030   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
  1031     .createInstance(Ci.nsIScriptableUnicodeConverter);
  1032   try {
  1033     converter.charset = aCharset || "UTF-8";
  1034     return converter.ConvertToUnicode(aString);
  1035   } catch(e) {
  1036     return aString;
  1040 /**
  1041  * Normalize multiple relative paths towards the base paths on the right.
  1042  */
  1043 function normalize(...aURLs) {
  1044   let base = Services.io.newURI(aURLs.pop(), null, null);
  1045   let url;
  1046   while ((url = aURLs.pop())) {
  1047     base = Services.io.newURI(url, null, base);
  1049   return base.spec;
  1052 function dirname(aPath) {
  1053   return Services.io.newURI(
  1054     ".", null, Services.io.newURI(aPath, null, null)).spec;

mercurial