|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 let { components, Cc, Ci, Cu } = require("chrome"); |
|
8 let Services = require("Services"); |
|
9 |
|
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 |
|
15 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
16 const events = require("sdk/event/core"); |
|
17 const protocol = require("devtools/server/protocol"); |
|
18 const {Arg, Option, method, RetVal, types} = protocol; |
|
19 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); |
|
20 |
|
21 loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); |
|
22 |
|
23 let TRANSITION_CLASS = "moz-styleeditor-transitioning"; |
|
24 let TRANSITION_DURATION_MS = 500; |
|
25 let TRANSITION_RULE = "\ |
|
26 :root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\ |
|
27 transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ |
|
28 transition-delay: 0ms !important;\ |
|
29 transition-timing-function: ease-out !important;\ |
|
30 transition-property: all !important;\ |
|
31 }"; |
|
32 |
|
33 let LOAD_ERROR = "error-load"; |
|
34 |
|
35 exports.register = function(handle) { |
|
36 handle.addTabActor(StyleEditorActor, "styleEditorActor"); |
|
37 handle.addGlobalActor(StyleEditorActor, "styleEditorActor"); |
|
38 }; |
|
39 |
|
40 exports.unregister = function(handle) { |
|
41 handle.removeTabActor(StyleEditorActor); |
|
42 handle.removeGlobalActor(StyleEditorActor); |
|
43 }; |
|
44 |
|
45 types.addActorType("old-stylesheet"); |
|
46 |
|
47 /** |
|
48 * Creates a StyleEditorActor. StyleEditorActor provides remote access to the |
|
49 * stylesheets of a document. |
|
50 */ |
|
51 let StyleEditorActor = protocol.ActorClass({ |
|
52 typeName: "styleeditor", |
|
53 |
|
54 /** |
|
55 * The window we work with, taken from the parent actor. |
|
56 */ |
|
57 get window() this.parentActor.window, |
|
58 |
|
59 /** |
|
60 * The current content document of the window we work with. |
|
61 */ |
|
62 get document() this.window.document, |
|
63 |
|
64 events: { |
|
65 "document-load" : { |
|
66 type: "documentLoad", |
|
67 styleSheets: Arg(0, "array:old-stylesheet") |
|
68 } |
|
69 }, |
|
70 |
|
71 form: function() |
|
72 { |
|
73 return { actor: this.actorID }; |
|
74 }, |
|
75 |
|
76 initialize: function (conn, tabActor) { |
|
77 protocol.Actor.prototype.initialize.call(this, null); |
|
78 |
|
79 this.parentActor = tabActor; |
|
80 |
|
81 // keep a map of sheets-to-actors so we don't create two actors for one sheet |
|
82 this._sheets = new Map(); |
|
83 }, |
|
84 |
|
85 /** |
|
86 * Destroy the current StyleEditorActor instance. |
|
87 */ |
|
88 destroy: function() |
|
89 { |
|
90 this._sheets.clear(); |
|
91 }, |
|
92 |
|
93 /** |
|
94 * Called by client when target navigates to a new document. |
|
95 * Adds load listeners to document. |
|
96 */ |
|
97 newDocument: method(function() { |
|
98 // delete previous document's actors |
|
99 this._clearStyleSheetActors(); |
|
100 |
|
101 // Note: listening for load won't be necessary once |
|
102 // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed |
|
103 if (this.document.readyState == "complete") { |
|
104 this._onDocumentLoaded(); |
|
105 } |
|
106 else { |
|
107 this.window.addEventListener("load", this._onDocumentLoaded, false); |
|
108 } |
|
109 return {}; |
|
110 }), |
|
111 |
|
112 /** |
|
113 * Event handler for document loaded event. Add actor for each stylesheet |
|
114 * and send an event notifying of the load |
|
115 */ |
|
116 _onDocumentLoaded: function(event) { |
|
117 if (event) { |
|
118 this.window.removeEventListener("load", this._onDocumentLoaded, false); |
|
119 } |
|
120 |
|
121 let documents = [this.document]; |
|
122 var forms = []; |
|
123 for (let doc of documents) { |
|
124 let sheetForms = this._addStyleSheets(doc.styleSheets); |
|
125 forms = forms.concat(sheetForms); |
|
126 // Recursively handle style sheets of the documents in iframes. |
|
127 for (let iframe of doc.getElementsByTagName("iframe")) { |
|
128 documents.push(iframe.contentDocument); |
|
129 } |
|
130 } |
|
131 |
|
132 events.emit(this, "document-load", forms); |
|
133 }, |
|
134 |
|
135 /** |
|
136 * Add all the stylesheets to the map and create an actor for each one |
|
137 * if not already created. Send event that there are new stylesheets. |
|
138 * |
|
139 * @param {[DOMStyleSheet]} styleSheets |
|
140 * Stylesheets to add |
|
141 * @return {[object]} |
|
142 * Array of actors for each StyleSheetActor created |
|
143 */ |
|
144 _addStyleSheets: function(styleSheets) |
|
145 { |
|
146 let sheets = []; |
|
147 for (let i = 0; i < styleSheets.length; i++) { |
|
148 let styleSheet = styleSheets[i]; |
|
149 sheets.push(styleSheet); |
|
150 |
|
151 // Get all sheets, including imported ones |
|
152 let imports = this._getImported(styleSheet); |
|
153 sheets = sheets.concat(imports); |
|
154 } |
|
155 let actors = sheets.map(this._createStyleSheetActor.bind(this)); |
|
156 |
|
157 return actors; |
|
158 }, |
|
159 |
|
160 /** |
|
161 * Create a new actor for a style sheet, if it hasn't already been created. |
|
162 * |
|
163 * @param {DOMStyleSheet} styleSheet |
|
164 * The style sheet to create an actor for. |
|
165 * @return {StyleSheetActor} |
|
166 * The actor for this style sheet |
|
167 */ |
|
168 _createStyleSheetActor: function(styleSheet) |
|
169 { |
|
170 if (this._sheets.has(styleSheet)) { |
|
171 return this._sheets.get(styleSheet); |
|
172 } |
|
173 let actor = new OldStyleSheetActor(styleSheet, this); |
|
174 |
|
175 this.manage(actor); |
|
176 this._sheets.set(styleSheet, actor); |
|
177 |
|
178 return actor; |
|
179 }, |
|
180 |
|
181 /** |
|
182 * Get all the stylesheets @imported from a stylesheet. |
|
183 * |
|
184 * @param {DOMStyleSheet} styleSheet |
|
185 * Style sheet to search |
|
186 * @return {array} |
|
187 * All the imported stylesheets |
|
188 */ |
|
189 _getImported: function(styleSheet) { |
|
190 let imported = []; |
|
191 |
|
192 for (let i = 0; i < styleSheet.cssRules.length; i++) { |
|
193 let rule = styleSheet.cssRules[i]; |
|
194 if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { |
|
195 // Associated styleSheet may be null if it has already been seen due to |
|
196 // duplicate @imports for the same URL. |
|
197 if (!rule.styleSheet) { |
|
198 continue; |
|
199 } |
|
200 imported.push(rule.styleSheet); |
|
201 |
|
202 // recurse imports in this stylesheet as well |
|
203 imported = imported.concat(this._getImported(rule.styleSheet)); |
|
204 } |
|
205 else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { |
|
206 // @import rules must precede all others except @charset |
|
207 break; |
|
208 } |
|
209 } |
|
210 return imported; |
|
211 }, |
|
212 |
|
213 /** |
|
214 * Clear all the current stylesheet actors in map. |
|
215 */ |
|
216 _clearStyleSheetActors: function() { |
|
217 for (let actor in this._sheets) { |
|
218 this.unmanage(this._sheets[actor]); |
|
219 } |
|
220 this._sheets.clear(); |
|
221 }, |
|
222 |
|
223 /** |
|
224 * Create a new style sheet in the document with the given text. |
|
225 * Return an actor for it. |
|
226 * |
|
227 * @param {object} request |
|
228 * Debugging protocol request object, with 'text property' |
|
229 * @return {object} |
|
230 * Object with 'styelSheet' property for form on new actor. |
|
231 */ |
|
232 newStyleSheet: method(function(text) { |
|
233 let parent = this.document.documentElement; |
|
234 let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); |
|
235 style.setAttribute("type", "text/css"); |
|
236 |
|
237 if (text) { |
|
238 style.appendChild(this.document.createTextNode(text)); |
|
239 } |
|
240 parent.appendChild(style); |
|
241 |
|
242 let actor = this._createStyleSheetActor(style.sheet); |
|
243 return actor; |
|
244 }, { |
|
245 request: { text: Arg(0, "string") }, |
|
246 response: { styleSheet: RetVal("old-stylesheet") } |
|
247 }) |
|
248 }); |
|
249 |
|
250 /** |
|
251 * The corresponding Front object for the StyleEditorActor. |
|
252 */ |
|
253 let StyleEditorFront = protocol.FrontClass(StyleEditorActor, { |
|
254 initialize: function(client, tabForm) { |
|
255 protocol.Front.prototype.initialize.call(this, client); |
|
256 this.actorID = tabForm.styleEditorActor; |
|
257 |
|
258 client.addActorPool(this); |
|
259 this.manage(this); |
|
260 }, |
|
261 |
|
262 getStyleSheets: function() { |
|
263 let deferred = promise.defer(); |
|
264 |
|
265 events.once(this, "document-load", (styleSheets) => { |
|
266 deferred.resolve(styleSheets); |
|
267 }); |
|
268 this.newDocument(); |
|
269 |
|
270 return deferred.promise; |
|
271 }, |
|
272 |
|
273 addStyleSheet: function(text) { |
|
274 return this.newStyleSheet(text); |
|
275 } |
|
276 }); |
|
277 |
|
278 /** |
|
279 * A StyleSheetActor represents a stylesheet on the server. |
|
280 */ |
|
281 let OldStyleSheetActor = protocol.ActorClass({ |
|
282 typeName: "old-stylesheet", |
|
283 |
|
284 events: { |
|
285 "property-change" : { |
|
286 type: "propertyChange", |
|
287 property: Arg(0, "string"), |
|
288 value: Arg(1, "json") |
|
289 }, |
|
290 "source-load" : { |
|
291 type: "sourceLoad", |
|
292 source: Arg(0, "string") |
|
293 }, |
|
294 "style-applied" : { |
|
295 type: "styleApplied" |
|
296 } |
|
297 }, |
|
298 |
|
299 toString: function() { |
|
300 return "[OldStyleSheetActor " + this.actorID + "]"; |
|
301 }, |
|
302 |
|
303 /** |
|
304 * Window of target |
|
305 */ |
|
306 get window() this._window || this.parentActor.window, |
|
307 |
|
308 /** |
|
309 * Document of target. |
|
310 */ |
|
311 get document() this.window.document, |
|
312 |
|
313 /** |
|
314 * URL of underlying stylesheet. |
|
315 */ |
|
316 get href() this.rawSheet.href, |
|
317 |
|
318 /** |
|
319 * Retrieve the index (order) of stylesheet in the document. |
|
320 * |
|
321 * @return number |
|
322 */ |
|
323 get styleSheetIndex() |
|
324 { |
|
325 if (this._styleSheetIndex == -1) { |
|
326 for (let i = 0; i < this.document.styleSheets.length; i++) { |
|
327 if (this.document.styleSheets[i] == this.rawSheet) { |
|
328 this._styleSheetIndex = i; |
|
329 break; |
|
330 } |
|
331 } |
|
332 } |
|
333 return this._styleSheetIndex; |
|
334 }, |
|
335 |
|
336 initialize: function(aStyleSheet, aParentActor, aWindow) { |
|
337 protocol.Actor.prototype.initialize.call(this, null); |
|
338 |
|
339 this.rawSheet = aStyleSheet; |
|
340 this.parentActor = aParentActor; |
|
341 this.conn = this.parentActor.conn; |
|
342 |
|
343 this._window = aWindow; |
|
344 |
|
345 // text and index are unknown until source load |
|
346 this.text = null; |
|
347 this._styleSheetIndex = -1; |
|
348 |
|
349 this._transitionRefCount = 0; |
|
350 |
|
351 // if this sheet has an @import, then it's rules are loaded async |
|
352 let ownerNode = this.rawSheet.ownerNode; |
|
353 if (ownerNode) { |
|
354 let onSheetLoaded = function(event) { |
|
355 ownerNode.removeEventListener("load", onSheetLoaded, false); |
|
356 this._notifyPropertyChanged("ruleCount"); |
|
357 }.bind(this); |
|
358 |
|
359 ownerNode.addEventListener("load", onSheetLoaded, false); |
|
360 } |
|
361 }, |
|
362 |
|
363 /** |
|
364 * Get the current state of the actor |
|
365 * |
|
366 * @return {object} |
|
367 * With properties of the underlying stylesheet, plus 'text', |
|
368 * 'styleSheetIndex' and 'parentActor' if it's @imported |
|
369 */ |
|
370 form: function(detail) { |
|
371 if (detail === "actorid") { |
|
372 return this.actorID; |
|
373 } |
|
374 |
|
375 let docHref; |
|
376 if (this.rawSheet.ownerNode) { |
|
377 if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { |
|
378 docHref = this.rawSheet.ownerNode.location.href; |
|
379 } |
|
380 if (this.rawSheet.ownerNode.ownerDocument) { |
|
381 docHref = this.rawSheet.ownerNode.ownerDocument.location.href; |
|
382 } |
|
383 } |
|
384 |
|
385 let form = { |
|
386 actor: this.actorID, // actorID is set when this actor is added to a pool |
|
387 href: this.href, |
|
388 nodeHref: docHref, |
|
389 disabled: this.rawSheet.disabled, |
|
390 title: this.rawSheet.title, |
|
391 system: !CssLogic.isContentStylesheet(this.rawSheet), |
|
392 styleSheetIndex: this.styleSheetIndex |
|
393 } |
|
394 |
|
395 try { |
|
396 form.ruleCount = this.rawSheet.cssRules.length; |
|
397 } |
|
398 catch(e) { |
|
399 // stylesheet had an @import rule that wasn't loaded yet |
|
400 } |
|
401 return form; |
|
402 }, |
|
403 |
|
404 /** |
|
405 * Toggle the disabled property of the style sheet |
|
406 * |
|
407 * @return {object} |
|
408 * 'disabled' - the disabled state after toggling. |
|
409 */ |
|
410 toggleDisabled: method(function() { |
|
411 this.rawSheet.disabled = !this.rawSheet.disabled; |
|
412 this._notifyPropertyChanged("disabled"); |
|
413 |
|
414 return this.rawSheet.disabled; |
|
415 }, { |
|
416 response: { disabled: RetVal("boolean")} |
|
417 }), |
|
418 |
|
419 /** |
|
420 * Send an event notifying that a property of the stylesheet |
|
421 * has changed. |
|
422 * |
|
423 * @param {string} property |
|
424 * Name of the changed property |
|
425 */ |
|
426 _notifyPropertyChanged: function(property) { |
|
427 events.emit(this, "property-change", property, this.form()[property]); |
|
428 }, |
|
429 |
|
430 /** |
|
431 * Fetch the source of the style sheet from its URL. Send a "sourceLoad" |
|
432 * event when it's been fetched. |
|
433 */ |
|
434 fetchSource: method(function() { |
|
435 this._getText().then((content) => { |
|
436 events.emit(this, "source-load", this.text); |
|
437 }); |
|
438 }), |
|
439 |
|
440 /** |
|
441 * Fetch the text for this stylesheet from the cache or network. Return |
|
442 * cached text if it's already been fetched. |
|
443 * |
|
444 * @return {Promise} |
|
445 * Promise that resolves with a string text of the stylesheet. |
|
446 */ |
|
447 _getText: function() { |
|
448 if (this.text) { |
|
449 return promise.resolve(this.text); |
|
450 } |
|
451 |
|
452 if (!this.href) { |
|
453 // this is an inline <style> sheet |
|
454 let content = this.rawSheet.ownerNode.textContent; |
|
455 this.text = content; |
|
456 return promise.resolve(content); |
|
457 } |
|
458 |
|
459 let options = { |
|
460 window: this.window, |
|
461 charset: this._getCSSCharset() |
|
462 }; |
|
463 |
|
464 return fetch(this.href, options).then(({ content }) => { |
|
465 this.text = content; |
|
466 return content; |
|
467 }); |
|
468 }, |
|
469 |
|
470 /** |
|
471 * Get the charset of the stylesheet according to the character set rules |
|
472 * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>. |
|
473 * |
|
474 * @param string channelCharset |
|
475 * Charset of the source string if set by the HTTP channel. |
|
476 */ |
|
477 _getCSSCharset: function(channelCharset) |
|
478 { |
|
479 // StyleSheet's charset can be specified from multiple sources |
|
480 if (channelCharset && channelCharset.length > 0) { |
|
481 // step 1 of syndata.html: charset given in HTTP header. |
|
482 return channelCharset; |
|
483 } |
|
484 |
|
485 let sheet = this.rawSheet; |
|
486 if (sheet) { |
|
487 // Do we have a @charset rule in the stylesheet? |
|
488 // step 2 of syndata.html (without the BOM check). |
|
489 if (sheet.cssRules) { |
|
490 let rules = sheet.cssRules; |
|
491 if (rules.length |
|
492 && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) { |
|
493 return rules.item(0).encoding; |
|
494 } |
|
495 } |
|
496 |
|
497 // step 3: charset attribute of <link> or <style> element, if it exists |
|
498 if (sheet.ownerNode && sheet.ownerNode.getAttribute) { |
|
499 let linkCharset = sheet.ownerNode.getAttribute("charset"); |
|
500 if (linkCharset != null) { |
|
501 return linkCharset; |
|
502 } |
|
503 } |
|
504 |
|
505 // step 4 (1 of 2): charset of referring stylesheet. |
|
506 let parentSheet = sheet.parentStyleSheet; |
|
507 if (parentSheet && parentSheet.cssRules && |
|
508 parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) { |
|
509 return parentSheet.cssRules[0].encoding; |
|
510 } |
|
511 |
|
512 // step 4 (2 of 2): charset of referring document. |
|
513 if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) { |
|
514 return sheet.ownerNode.ownerDocument.characterSet; |
|
515 } |
|
516 } |
|
517 |
|
518 // step 5: default to utf-8. |
|
519 return "UTF-8"; |
|
520 }, |
|
521 |
|
522 /** |
|
523 * Update the style sheet in place with new text. |
|
524 * |
|
525 * @param {object} request |
|
526 * 'text' - new text |
|
527 * 'transition' - whether to do CSS transition for change. |
|
528 */ |
|
529 update: method(function(text, transition) { |
|
530 DOMUtils.parseStyleSheet(this.rawSheet, text); |
|
531 |
|
532 this.text = text; |
|
533 |
|
534 this._notifyPropertyChanged("ruleCount"); |
|
535 |
|
536 if (transition) { |
|
537 this._insertTransistionRule(); |
|
538 } |
|
539 else { |
|
540 this._notifyStyleApplied(); |
|
541 } |
|
542 }, { |
|
543 request: { |
|
544 text: Arg(0, "string"), |
|
545 transition: Arg(1, "boolean") |
|
546 } |
|
547 }), |
|
548 |
|
549 /** |
|
550 * Insert a catch-all transition rule into the document. Set a timeout |
|
551 * to remove the rule after a certain time. |
|
552 */ |
|
553 _insertTransistionRule: function() { |
|
554 // Insert the global transition rule |
|
555 // Use a ref count to make sure we do not add it multiple times.. and remove |
|
556 // it only when all pending StyleEditor-generated transitions ended. |
|
557 if (this._transitionRefCount == 0) { |
|
558 this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length); |
|
559 this.document.documentElement.classList.add(TRANSITION_CLASS); |
|
560 } |
|
561 |
|
562 this._transitionRefCount++; |
|
563 |
|
564 // Set up clean up and commit after transition duration (+10% buffer) |
|
565 // @see _onTransitionEnd |
|
566 this.window.setTimeout(this._onTransitionEnd.bind(this), |
|
567 Math.floor(TRANSITION_DURATION_MS * 1.1)); |
|
568 }, |
|
569 |
|
570 /** |
|
571 * This cleans up class and rule added for transition effect and then |
|
572 * notifies that the style has been applied. |
|
573 */ |
|
574 _onTransitionEnd: function() |
|
575 { |
|
576 if (--this._transitionRefCount == 0) { |
|
577 this.document.documentElement.classList.remove(TRANSITION_CLASS); |
|
578 this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1); |
|
579 } |
|
580 |
|
581 events.emit(this, "style-applied"); |
|
582 } |
|
583 }) |
|
584 |
|
585 /** |
|
586 * StyleSheetFront is the client-side counterpart to a StyleSheetActor. |
|
587 */ |
|
588 var OldStyleSheetFront = protocol.FrontClass(OldStyleSheetActor, { |
|
589 initialize: function(conn, form, ctx, detail) { |
|
590 protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail); |
|
591 |
|
592 this._onPropertyChange = this._onPropertyChange.bind(this); |
|
593 events.on(this, "property-change", this._onPropertyChange); |
|
594 }, |
|
595 |
|
596 destroy: function() { |
|
597 events.off(this, "property-change", this._onPropertyChange); |
|
598 |
|
599 protocol.Front.prototype.destroy.call(this); |
|
600 }, |
|
601 |
|
602 _onPropertyChange: function(property, value) { |
|
603 this._form[property] = value; |
|
604 }, |
|
605 |
|
606 form: function(form, detail) { |
|
607 if (detail === "actorid") { |
|
608 this.actorID = form; |
|
609 return; |
|
610 } |
|
611 this.actorID = form.actor; |
|
612 this._form = form; |
|
613 }, |
|
614 |
|
615 getText: function() { |
|
616 let deferred = promise.defer(); |
|
617 |
|
618 events.once(this, "source-load", (source) => { |
|
619 let longStr = new ShortLongString(source); |
|
620 deferred.resolve(longStr); |
|
621 }); |
|
622 this.fetchSource(); |
|
623 |
|
624 return deferred.promise; |
|
625 }, |
|
626 |
|
627 getOriginalSources: function() { |
|
628 return promise.resolve([]); |
|
629 }, |
|
630 |
|
631 get href() this._form.href, |
|
632 get nodeHref() this._form.nodeHref, |
|
633 get disabled() !!this._form.disabled, |
|
634 get title() this._form.title, |
|
635 get isSystem() this._form.system, |
|
636 get styleSheetIndex() this._form.styleSheetIndex, |
|
637 get ruleCount() this._form.ruleCount |
|
638 }); |
|
639 |
|
640 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { |
|
641 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); |
|
642 }); |
|
643 |
|
644 exports.StyleEditorActor = StyleEditorActor; |
|
645 exports.StyleEditorFront = StyleEditorFront; |
|
646 |
|
647 exports.OldStyleSheetActor = OldStyleSheetActor; |
|
648 exports.OldStyleSheetFront = OldStyleSheetFront; |
|
649 |
|
650 |
|
651 /** |
|
652 * Performs a request to load the desired URL and returns a promise. |
|
653 * |
|
654 * @param aURL String |
|
655 * The URL we will request. |
|
656 * @returns Promise |
|
657 * A promise of the document at that URL, as a string. |
|
658 */ |
|
659 function fetch(aURL, aOptions={ loadFromCache: true, window: null, |
|
660 charset: null}) { |
|
661 let deferred = promise.defer(); |
|
662 let scheme; |
|
663 let url = aURL.split(" -> ").pop(); |
|
664 let charset; |
|
665 let contentType; |
|
666 |
|
667 try { |
|
668 scheme = Services.io.extractScheme(url); |
|
669 } catch (e) { |
|
670 // In the xpcshell tests, the script url is the absolute path of the test |
|
671 // file, which will make a malformed URI error be thrown. Add the file |
|
672 // scheme prefix ourselves. |
|
673 url = "file://" + url; |
|
674 scheme = Services.io.extractScheme(url); |
|
675 } |
|
676 |
|
677 switch (scheme) { |
|
678 case "file": |
|
679 case "chrome": |
|
680 case "resource": |
|
681 try { |
|
682 NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) { |
|
683 if (!components.isSuccessCode(aStatus)) { |
|
684 deferred.reject(new Error("Request failed with status code = " |
|
685 + aStatus |
|
686 + " after NetUtil.asyncFetch for url = " |
|
687 + url)); |
|
688 return; |
|
689 } |
|
690 |
|
691 let source = NetUtil.readInputStreamToString(aStream, aStream.available()); |
|
692 contentType = aRequest.contentType; |
|
693 deferred.resolve(source); |
|
694 aStream.close(); |
|
695 }); |
|
696 } catch (ex) { |
|
697 deferred.reject(ex); |
|
698 } |
|
699 break; |
|
700 |
|
701 default: |
|
702 let channel; |
|
703 try { |
|
704 channel = Services.io.newChannel(url, null, null); |
|
705 } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") { |
|
706 // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but |
|
707 // newChannel won't be able to handle it. |
|
708 url = "file:///" + url; |
|
709 channel = Services.io.newChannel(url, null, null); |
|
710 } |
|
711 let chunks = []; |
|
712 let streamListener = { |
|
713 onStartRequest: function(aRequest, aContext, aStatusCode) { |
|
714 if (!components.isSuccessCode(aStatusCode)) { |
|
715 deferred.reject(new Error("Request failed with status code = " |
|
716 + aStatusCode |
|
717 + " in onStartRequest handler for url = " |
|
718 + url)); |
|
719 } |
|
720 }, |
|
721 onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { |
|
722 chunks.push(NetUtil.readInputStreamToString(aStream, aCount)); |
|
723 }, |
|
724 onStopRequest: function(aRequest, aContext, aStatusCode) { |
|
725 if (!components.isSuccessCode(aStatusCode)) { |
|
726 deferred.reject(new Error("Request failed with status code = " |
|
727 + aStatusCode |
|
728 + " in onStopRequest handler for url = " |
|
729 + url)); |
|
730 return; |
|
731 } |
|
732 |
|
733 charset = channel.contentCharset || charset; |
|
734 contentType = channel.contentType; |
|
735 deferred.resolve(chunks.join("")); |
|
736 } |
|
737 }; |
|
738 |
|
739 if (aOptions.window) { |
|
740 // respect private browsing |
|
741 channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor) |
|
742 .getInterface(Ci.nsIWebNavigation) |
|
743 .QueryInterface(Ci.nsIDocumentLoader) |
|
744 .loadGroup; |
|
745 } |
|
746 channel.loadFlags = aOptions.loadFromCache |
|
747 ? channel.LOAD_FROM_CACHE |
|
748 : channel.LOAD_BYPASS_CACHE; |
|
749 channel.asyncOpen(streamListener, null); |
|
750 break; |
|
751 } |
|
752 |
|
753 return deferred.promise.then(source => { |
|
754 return { |
|
755 content: convertToUnicode(source, charset), |
|
756 contentType: contentType |
|
757 }; |
|
758 }); |
|
759 } |
|
760 |
|
761 /** |
|
762 * Convert a given string, encoded in a given character set, to unicode. |
|
763 * |
|
764 * @param string aString |
|
765 * A string. |
|
766 * @param string aCharset |
|
767 * A character set. |
|
768 */ |
|
769 function convertToUnicode(aString, aCharset=null) { |
|
770 // Decoding primitives. |
|
771 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
772 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
773 try { |
|
774 converter.charset = aCharset || "UTF-8"; |
|
775 return converter.ConvertToUnicode(aString); |
|
776 } catch(e) { |
|
777 return aString; |
|
778 } |
|
779 } |
|
780 |
|
781 /** |
|
782 * Normalize multiple relative paths towards the base paths on the right. |
|
783 */ |
|
784 function normalize(...aURLs) { |
|
785 let base = Services.io.newURI(aURLs.pop(), null, null); |
|
786 let url; |
|
787 while ((url = aURLs.pop())) { |
|
788 base = Services.io.newURI(url, null, base); |
|
789 } |
|
790 return base.spec; |
|
791 } |
|
792 |
|
793 function dirname(aPath) { |
|
794 return Services.io.newURI( |
|
795 ".", null, Services.io.newURI(aPath, null, null)).spec; |
|
796 } |