Wed, 31 Dec 2014 06:09:35 +0100
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;
1004 }
1005 channel.loadFlags = aOptions.loadFromCache
1006 ? channel.LOAD_FROM_CACHE
1007 : channel.LOAD_BYPASS_CACHE;
1008 channel.asyncOpen(streamListener, null);
1009 break;
1010 }
1012 return deferred.promise.then(source => {
1013 return {
1014 content: convertToUnicode(source, charset),
1015 contentType: contentType
1016 };
1017 });
1018 }
1020 /**
1021 * Convert a given string, encoded in a given character set, to unicode.
1022 *
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;
1037 }
1038 }
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);
1048 }
1049 return base.spec;
1050 }
1052 function dirname(aPath) {
1053 return Services.io.newURI(
1054 ".", null, Services.io.newURI(aPath, null, null)).spec;
1055 }