1 /* See license.txt for terms of usage */
  2 
  3 FBL.ns(function() { with (FBL) {
  4 
  5 // ************************************************************************************************
  6 // Constants
  7 
  8 const Cc = Components.classes;
  9 const Ci = Components.interfaces;
 10 
 11 // List of contexts with XHR spy attached.
 12 var contexts = [];
 13 
 14 // ************************************************************************************************
 15 // Spy Module
 16 
 17 /**
 18  * @module Represents a XHR Spy module. The main purpose of the XHR Spy feature is to monitor
 19  * XHR activity of the current page and create appropriate log into the Console panel.
 20  * This feature can be controlled by an option <i>Show XMLHttpRequests</i> (from within the
 21  * console panel).
 22  * 
 23  * The module is responsible for attaching/detaching a HTTP Observers when Firebug is
 24  * activated/deactivated for a site.
 25  */
 26 Firebug.Spy = extend(Firebug.Module,
 27 /** @lends Firebug.Spy */
 28 {
 29     dispatchName: "spy",
 30 
 31     initialize: function()
 32     {
 33         if (Firebug.TraceModule)
 34             Firebug.TraceModule.addListener(this.TraceListener);
 35 
 36         Firebug.Module.initialize.apply(this, arguments);
 37     },
 38 
 39     shutdown: function()
 40     {
 41         Firebug.Module.shutdown.apply(this, arguments);
 42 
 43         if (Firebug.TraceModule)
 44             Firebug.TraceModule.removeListener(this.TraceListener);
 45     },
 46 
 47     initContext: function(context)
 48     {
 49         context.spies = [];
 50 
 51         if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
 52             this.attachObserver(context, context.window);
 53 
 54         if (FBTrace.DBG_SPY)
 55             FBTrace.sysout("spy.initContext " + contexts.length + " ", context.getName());
 56     },
 57 
 58     destroyContext: function(context)
 59     {
 60         // For any spies that are in progress, remove our listeners so that they don't leak
 61         this.detachObserver(context, null);
 62 
 63         if (FBTrace.DBG_SPY && context.spies.length)
 64             FBTrace.sysout("spy.destroyContext; ERROR There are leaking Spies ("
 65                 + context.spies.length + ") " + context.getName());
 66 
 67         delete context.spies;
 68 
 69         if (FBTrace.DBG_SPY)
 70             FBTrace.sysout("spy.destroyContext " + contexts.length + " ", context.getName());
 71     },
 72 
 73     watchWindow: function(context, win)
 74     {
 75         if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
 76             this.attachObserver(context, win);
 77     },
 78 
 79     unwatchWindow: function(context, win)
 80     {
 81         try
 82         {
 83             // This make sure that the existing context is properly removed from "contexts" array.
 84             this.detachObserver(context, win);
 85         }
 86         catch (ex)
 87         {
 88             // Get exceptions here sometimes, so let's just ignore them
 89             // since the window is going away anyhow
 90             ERROR(ex);
 91         }
 92     },
 93 
 94     updateOption: function(name, value)
 95     {
 96         // XXXjjb Honza, if Console.isEnabled(context) false, then this can't be called,
 97         // but somehow seems not correct
 98         if (name == "showXMLHttpRequests")
 99         {
100             var tach = value ? this.attachObserver : this.detachObserver;
101             for (var i = 0; i < TabWatcher.contexts.length; ++i)
102             {
103                 var context = TabWatcher.contexts[i];
104                 iterateWindows(context.window, function(win)
105                 {
106                     tach.apply(this, [context, win]);
107                 });
108             }
109         }
110     },
111 
112     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
113     // Attaching Spy to XHR requests.
114 
115     /**
116      * Returns false if Spy should not be attached to XHRs executed by the specified window.
117      */
118     skipSpy: function(win)
119     {
120         if (!win)
121             return true;
122 
123         // Don't attach spy to chrome.
124         var uri = safeGetWindowLocation(win);
125         if (uri && (uri.indexOf("about:") == 0 || uri.indexOf("chrome:") == 0))
126             return true;
127     },
128 
129     attachObserver: function(context, win)
130     {
131         if (Firebug.Spy.skipSpy(win))
132             return;
133 
134         for (var i=0; i<contexts.length; ++i)
135         {
136             if ((contexts[i].context == context) && (contexts[i].win == win))
137                 return;
138         }
139 
140         // Register HTTP observers only once.
141         if (contexts.length == 0)
142         {
143             httpObserver.addObserver(SpyHttpObserver, "firebug-http-event", false);
144             SpyHttpActivityObserver.registerObserver();
145         }
146 
147         contexts.push({context: context, win: win});
148 
149         if (FBTrace.DBG_SPY)
150             FBTrace.sysout("spy.attachObserver (HTTP) " + contexts.length + " ", context.getName());
151     },
152 
153     detachObserver: function(context, win)
154     {
155         for (var i=0; i<contexts.length; ++i)
156         {
157             if (contexts[i].context == context)
158             {
159                 if (win && (contexts[i].win != win))
160                     continue;
161 
162                 contexts.splice(i, 1);
163 
164                 // If no context is using spy, remvove the (only one) HTTP observer.
165                 if (contexts.length == 0)
166                 {
167                     httpObserver.removeObserver(SpyHttpObserver, "firebug-http-event");
168                     SpyHttpActivityObserver.unregisterObserver();
169                 }
170 
171                 if (FBTrace.DBG_SPY)
172                     FBTrace.sysout("spy.detachObserver (HTTP) " + contexts.length + " ",
173                         context.getName());
174                 return;
175             }
176         }
177     },
178 
179     /**
180      * Return XHR object that is associated with specified request <i>nsIHttpChannel</i>.
181      * Returns null if the request doesn't represent XHR.
182      */
183     getXHR: function(request)
184     {
185         // Does also query-interface for nsIHttpChannel.
186         if (!(request instanceof Ci.nsIHttpChannel))
187             return null;
188 
189         try
190         {
191             var callbacks = request.notificationCallbacks;
192             return (callbacks ? callbacks.getInterface(Ci.nsIXMLHttpRequest) : null);
193         }
194         catch (exc)
195         {
196             if (exc.name == "NS_NOINTERFACE")
197             {
198                 if (FBTrace.DBG_SPY)
199                     FBTrace.sysout("spy.getXHR; Request is not nsIXMLHttpRequest: " +
200                         safeGetRequestName(request));
201             }
202         }
203 
204        return null;
205     },
206 });
207 
208 // ************************************************************************************************
209 
210 /**
211  * @class This observer uses {@link HttpRequestObserver} to monitor start and end of all XHRs.
212  * using <code>http-on-modify-request</code>, <code>http-on-examine-response</code> and
213  * <code>http-on-examine-cached-response</code> events. For every monitored XHR a new 
214  * instance of {@link Firebug.Spy.XMLHttpRequestSpy} object is created. This instance is removed
215  * when the XHR is finished.
216  */
217 var SpyHttpObserver =
218 /** @lends SpyHttpObserver */
219 {
220     observe: function(request, topic, data)
221     {
222         try
223         {
224             if (topic != "http-on-modify-request" &&
225                 topic != "http-on-examine-response" &&
226                 topic != "http-on-examine-cached-response")
227             {
228                 if (FBTrace.DBG_ERRORS || FBTrace.DBG_SPY)
229                     FBTrace.sysout("spy.SpyHttpObserver.observe; ERROR Unknown topic: " + topic);
230                 return;
231             }
232 
233             this.observeRequest(request, topic);
234         }
235         catch (exc)
236         {
237             if (FBTrace.DBG_ERRORS || FBTrace.DBG_SPY)
238                 FBTrace.sysout("spy.SpyHttpObserver EXCEPTION", exc);
239         }
240     },
241 
242     observeRequest: function(request, topic)
243     {
244         var win = getWindowForRequest(request);
245         var xhr = Firebug.Spy.getXHR(request);
246 
247         // The request must be associated with window (i.e. tab) and it also must be 
248         // real XHR request.
249         if (!win || !xhr)
250             return;
251 
252         for (var i=0; i<contexts.length; ++i)
253         {
254             var context = contexts[i];
255             if (context.win == win)
256             {
257                 var spyContext = context.context;
258                 var requestName = request.URI.asciiSpec;
259                 var requestMethod = request.requestMethod;
260 
261                 if (topic == "http-on-modify-request")
262                     this.requestStarted(request, xhr, spyContext, requestMethod, requestName);
263                 else if (topic == "http-on-examine-response")
264                     this.requestStopped(request, xhr, spyContext, requestMethod, requestName);
265                 else if (topic == "http-on-examine-cached-response")
266                     this.requestStopped(request, xhr, spyContext, requestMethod, requestName);
267 
268                 return;
269             }
270         }
271     },
272 
273     requestStarted: function(request, xhr, context, method, url)
274     {
275         var spy = getSpyForXHR(request, xhr, context);
276         spy.method = method;
277         spy.href = url;
278 
279         if (FBTrace.DBG_SPY)
280             FBTrace.sysout("spy.requestStarted; " + spy.href, spy);
281 
282         // Get "body" for POST and PUT requests. It will be displayed in
283         // appropriate tab of the XHR.
284         if (method == "POST" || method == "PUT")
285             spy.postText = readPostTextFromRequest(request, context);
286 
287         spy.urlParams = parseURLParams(spy.href);
288 
289         // In case of redirects there is no stack and the source link is null.
290         spy.sourceLink = getStackSourceLink();
291 
292         if (!spy.requestHeaders)
293             spy.requestHeaders = getRequestHeaders(spy);
294 
295         // If it's enabled log the request into the console tab.
296         if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
297         {
298             spy.logRow = Firebug.Console.log(spy, spy.context, "spy", null, true);
299             setClass(spy.logRow, "loading");
300         }
301 
302         // Notify registered listeners. The onStart event is fired once for entire XHR
303         // (even if there is more redirects within the process).
304         var name = request.URI.asciiSpec;
305         var origName = request.originalURI.asciiSpec;
306         if (name == origName)
307             dispatch(Firebug.Spy.fbListeners, "onStart", [context, spy]);
308 
309         // Remember the start time et the end, so it's most accurate.
310         spy.sendTime = new Date().getTime();
311     },
312 
313     requestStopped: function(request, xhr, context, method, url)
314     {
315         var spy = getSpyForXHR(request, xhr, context);
316         if (!spy)
317             return;
318 
319         spy.endTime = new Date().getTime();
320         spy.responseTime = spy.endTime - spy.sendTime;
321         spy.mimeType = Firebug.NetMonitor.Utils.getMimeType(request.contentType, request.name);
322 
323         if (!spy.responseHeaders)
324             spy.responseHeaders = getResponseHeaders(spy);
325 
326         if (!spy.statusText)
327         {
328             try
329             {
330                 spy.statusCode = request.responseStatus;
331                 spy.statusText = request.responseStatusText;
332             }
333             catch (exc)
334             {
335                 if (FBTrace.DBG_SPY)
336                     FBTrace.sysout("spy.requestStopped " + spy.href + ", status access FAILED", exc);
337             }
338         }
339 
340         if (spy.logRow)
341         {
342             updateLogRow(spy);
343             updateHttpSpyInfo(spy);
344         }
345 
346         // Remove only the Spy object that has been created for an intermediate rediret
347         // request. These exist only to be also displayed in the console and they
348         // don't attach any listeners to the original XHR object (which is always created
349         // only once even in case of redirects).
350         // xxxHonza: These requests are not observer by the activityObserver now
351         // (if they should be observed we have to remove them in the activityObserver)
352         if (!spy.onLoad && spy.context.spies)
353             remove(spy.context.spies, spy);
354 
355         if (FBTrace.DBG_SPY)
356             FBTrace.sysout("spy.requestStopped: " + spy.href + ", responseTime: " +
357                 spy.responseTime + "ms, spy.responseText: " +
358                 (spy.reponseText ? spy.responseText.length : 0) + " bytes");
359     }
360 };
361 
362 // ************************************************************************************************
363 // Activity Observer
364 
365 /**
366  * @class This observer is used to properly monitor even mulipart XHRs. It's based on
367  * an activity-observer component that has been introduced in Firefox 3.6.
368  */
369 var SpyHttpActivityObserver = extend(Firebug.NetMonitor.NetHttpActivityObserver,
370 /** @lends SpyHttpActivityObserver */
371 {
372     activeRequests: [],
373 
374     observeRequest: function(request, activityType, activitySubtype, timestamp,
375         extraSizeData, extraStringData)
376     {
377         if (activityType != Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION &&
378            (activityType == Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_SOCKET_TRANSPORT &&
379             activitySubtype != Ci.nsISocketTransport.STATUS_RECEIVING_FROM))
380             return;
381 
382         var win = getWindowForRequest(request);
383         if (!win)
384         {
385             var index = this.activeRequests.indexOf(request);
386             if (!(win = this.activeRequests[index+1]))
387                 return;
388         }
389 
390         for (var i=0; i<contexts.length; ++i)
391         {
392             var context = contexts[i];
393             if (context.win == win)
394             {
395                 var spyContext = context.context;
396                 var spy = getSpyForXHR(request, null, spyContext, true);
397                 if (spy)
398                     this.observeXHRActivity(win, spy, request, activitySubtype, timestamp);
399                 return;
400             }
401         }
402     },
403 
404     observeXHRActivity: function(win, spy, request, activitySubtype, timestamp)
405     {
406         // Activity observer has precise time info so, use it.
407         var time = new Date();
408         time.setTime(timestamp/1000);
409 
410         if (activitySubtype == Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_REQUEST_HEADER)
411         {
412             if (FBTrace.DBG_SPY)
413                 FBTrace.sysout("spy.observeXHRActivity REQUEST_HEADER " + safeGetRequestName(request));
414 
415             this.activeRequests.push(request);
416             this.activeRequests.push(win);
417 
418             spy.sendTime = time;
419             spy.transactionStarted = true;
420         }
421         else if (activitySubtype == Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
422         {
423             if (FBTrace.DBG_SPY)
424                 FBTrace.sysout("spy.observeXHRActivity TRANSACTION_CLOSE " + safeGetRequestName(request));
425 
426             var index = this.activeRequests.indexOf(request);
427             this.activeRequests.splice(index, 2);
428 
429             spy.endTime = time;
430             spy.transactionClosed = true;
431 
432             // This should be the proper time to detach the Spy object, but only
433             // in the case when the XHR is already loaded. If the XHR is made as part of the
434             // page load, it may happen that the event (readyState == 4) comes later
435             // than actual TRANSACTION_CLOSE.
436             if (spy.loaded)
437                 spy.detach();
438         }
439         else if (activitySubtype == Ci.nsISocketTransport.STATUS_RECEIVING_FROM)
440         {
441             spy.endTime = time;
442         }
443     }
444 });
445 
446 // ************************************************************************************************
447 
448 function getSpyForXHR(request, xhrRequest, context, noCreate)
449 {
450     var spy = null;
451 
452     // Iterate all existing spy objects in this context and look for one that is
453     // already created for this request.
454     var length = context.spies.length;
455     for (var i=0; i<length; i++)
456     {
457         spy = context.spies[i];
458         if (spy.request == request)
459             return spy;
460     }
461 
462     if (noCreate)
463         return null;
464 
465     spy = new Firebug.Spy.XMLHttpRequestSpy(request, xhrRequest, context);
466     context.spies.push(spy);
467 
468     var name = request.URI.asciiSpec;
469     var origName = request.originalURI.asciiSpec;
470 
471     // Attach spy only to the original request. Notice that there can be more network requests
472     // made by the same XHR if redirects are involved.
473     if (name == origName)
474         spy.attach();
475 
476     if (FBTrace.DBG_SPY)
477         FBTrace.sysout("spy.getSpyForXHR; New spy object created (" +
478             (name == origName ? "new XHR" : "redirected XHR") + ") for: " + name, spy);
479 
480     return spy;
481 }
482 
483 // ************************************************************************************************
484 
485 /**
486  * @class This class represents a Spy object that is attached to XHR. This object
487  * registers various listeners into the XHR in order to monitor various events fired
488  * during the request process (onLoad, onAbort, etc.)
489  */
490 Firebug.Spy.XMLHttpRequestSpy = function(request, xhrRequest, context)
491 {
492     this.request = request;
493     this.xhrRequest = xhrRequest;
494     this.context = context;
495     this.responseText = "";
496 
497     // For compatibility with the Net templates.
498     this.isXHR = true;
499 
500     // Support for activity-observer
501     this.transactionStarted = false;
502     this.transactionClosed = false;
503 };
504 
505 Firebug.Spy.XMLHttpRequestSpy.prototype =
506 /** @lends Firebug.Spy.XMLHttpRequestSpy */
507 {
508     attach: function()
509     {
510         var spy = this;
511         this.onReadyStateChange = function(event) { onHTTPSpyReadyStateChange(spy, event); };
512         this.onLoad = function() { onHTTPSpyLoad(spy); };
513         this.onError = function() { onHTTPSpyError(spy); };
514         this.onAbort = function() { onHTTPSpyAbort(spy); };
515 
516         // xxxHonza: #502959 is still failing on Fx 3.5
517         // Use activity distributor to identify 3.6 
518         if (SpyHttpActivityObserver.getActivityDistributor())
519         {
520             this.onreadystatechange = this.xhrRequest.onreadystatechange;
521             this.xhrRequest.onreadystatechange = this.onReadyStateChange;
522         }
523 
524         this.xhrRequest.addEventListener("load", this.onLoad, false);
525         this.xhrRequest.addEventListener("error", this.onError, false);
526         this.xhrRequest.addEventListener("abort", this.onAbort, false);
527 
528         // xxxHonza: should be removed from FB 3.6
529         if (!SpyHttpActivityObserver.getActivityDistributor())
530             this.context.sourceCache.addListener(this);
531     },
532 
533     detach: function()
534     {
535         // Bubble out if already detached.
536         if (!this.onLoad)
537             return;
538 
539         // If the activity distributor is available, let's detach it when the XHR
540         // transaction is closed. Since, in case of multipart XHRs the onLoad method
541         // (readyState == 4) can be called mutliple times.
542         // Keep in mind:
543         // 1) It can happen that that the TRANSACTION_CLOSE event comes before
544         // the onLoad (if the XHR is made as part of the page load) so, detach if
545         // it's already closed.
546         // 2) In case of immediate cache responses, the transaction doesn't have to
547         // be started at all (or the activity observer is no available in Firefox 3.5).
548         // So, also detach in this case.
549         if (this.transactionStarted && !this.transactionClosed)
550             return;
551 
552         if (FBTrace.DBG_SPY)
553             FBTrace.sysout("spy.detach; " + this.href);
554 
555         // Remove itself from the list of active spies.
556         remove(this.context.spies, this);
557 
558         if (this.onreadystatechange)
559             this.xhrRequest.onreadystatechange = this.onreadystatechange;
560 
561         try { this.xhrRequest.removeEventListener("load", this.onLoad, false); } catch (e) {}
562         try { this.xhrRequest.removeEventListener("error", this.onError, false); } catch (e) {}
563         try { this.xhrRequest.removeEventListener("abort", this.onAbort, false); } catch (e) {}
564 
565         this.onreadystatechange = null;
566         this.onLoad = null;
567         this.onError = null;
568         this.onAbort = null;
569 
570         // xxxHonza: shouuld be removed from FB 1.6
571         if (!SpyHttpActivityObserver.getActivityDistributor())
572             this.context.sourceCache.removeListener(this);
573     },
574 
575     getURL: function()
576     {
577         return this.xhrRequest.channel ? this.xhrRequest.channel.name : this.href;
578     },
579 
580     // Cache listener
581     onStopRequest: function(context, request, responseText)
582     {
583         if (!responseText)
584             return;
585 
586         if (request == this.request)
587             this.responseText = responseText;
588     },
589 };
590 
591 // ************************************************************************************************
592 
593 function onHTTPSpyReadyStateChange(spy, event)
594 {
595     if (FBTrace.DBG_SPY)
596         FBTrace.sysout("spy.onHTTPSpyReadyStateChange " + spy.xhrRequest.readyState +
597             " (multipart: " + spy.xhrRequest.multipart + ")");
598 
599     // Remember just in case spy is detached (readyState == 4).
600     var originalHandler = spy.onreadystatechange;
601 
602     // Force response text to be updated in the UI (in case the console entry
603     // has been already expanded and the response tab selected).
604     if (spy.logRow && spy.xhrRequest.readyState >= 3)
605     {
606         var netInfoBox = getChildByClass(spy.logRow, "spyHead", "netInfoBody");
607         if (netInfoBox)
608         {
609             netInfoBox.htmlPresented = false;
610             netInfoBox.responsePresented = false;
611         }
612     }
613 
614     // If the request is loading update the end time.
615     if (spy.xhrRequest.readyState == 3)
616     {
617         spy.responseTime = spy.endTime - spy.sendTime;
618         updateTime(spy);
619     }
620 
621     // Request loaded. Get all the info from the request now, just in case the 
622     // XHR would be aborted in the original onReadyStateChange handler.
623     if (spy.xhrRequest.readyState == 4)
624     {
625         // Cumulate response so, multipart response content is properly displayed.
626         if (SpyHttpActivityObserver.getActivityDistributor())
627             spy.responseText += spy.xhrRequest.responseText;
628         else
629         {
630             // xxxHonza: remove from FB 1.6
631             if (!spy.responseText)
632                 spy.responseText = spy.xhrRequest.responseText;
633         }
634 
635         // The XHR is loaded now (used also by the activity observer).
636         spy.loaded = true;
637 
638         // Update UI.
639         updateHttpSpyInfo(spy);
640 
641         // Notify Net pane about a request beeing loaded.
642         // xxxHonza: I don't think this is necessary.
643         var netProgress = spy.context.netProgress;
644         if (netProgress)
645             netProgress.post(netProgress.stopFile, [spy.request, spy.endTime, spy.postText, spy.responseText]);
646 
647         // Notify registered listeners about finish of the XHR.
648         dispatch(Firebug.Spy.fbListeners, "onLoad", [spy.context, spy]);
649     }
650 
651     // Pass the event to the original page handler.
652     callPageHandler(spy, event, originalHandler);
653 }
654 
655 function onHTTPSpyLoad(spy)
656 {
657     if (FBTrace.DBG_SPY)
658         FBTrace.sysout("spy.onHTTPSpyLoad: " + spy.href, spy);
659 
660     // Detach must be done in onLoad (not in onreadystatechange) otherwise
661     // onAbort would not be handled.
662     spy.detach();
663 
664     // xxxHonza: Still needed for Fx 3.5 (#502959)
665     if (!SpyHttpActivityObserver.getActivityDistributor())
666         onHTTPSpyReadyStateChange(spy, null);
667 }
668 
669 function onHTTPSpyError(spy)
670 {
671     if (FBTrace.DBG_SPY)
672         FBTrace.sysout("spy.onHTTPSpyError; " + spy.href, spy);
673 
674     spy.detach();
675     spy.loaded = true;
676 
677     if (spy.logRow)
678     {
679         removeClass(spy.logRow, "loading");
680         setClass(spy.logRow, "error");
681     }
682 }
683 
684 function onHTTPSpyAbort(spy)
685 {
686     if (FBTrace.DBG_SPY)
687         FBTrace.sysout("spy.onHTTPSpyAbort: " + spy.href, spy);
688 
689     spy.detach();
690     spy.loaded = true;
691 
692     if (spy.logRow)
693     {
694         removeClass(spy.logRow, "loading");
695         setClass(spy.logRow, "error");
696     }
697 
698     spy.statusText = "Aborted";
699     updateLogRow(spy);
700 
701     // Notify Net pane about a request beeing aborted.
702     // xxxHonza: the net panel shoud find out this itself.
703     var netProgress = spy.context.netProgress;
704     if (netProgress)
705         netProgress.post(netProgress.abortFile, [spy.request, spy.endTime, spy.postText, spy.responseText]);
706 }
707 
708 // ************************************************************************************************
709 
710 function callPageHandler(spy, event, originalHandler)
711 {
712     try
713     {
714         // Calling the page handler throwed an exception (see #502959)
715         // This should be fixed in Firefox 3.5
716         if (originalHandler)
717             originalHandler.handleEvent(event);
718     }
719     catch (exc)
720     {
721         if (FBTrace.DBG_ERRORS)
722             FBTrace.sysout("spy.onHTTPSpyReadyStateChange: EXCEPTION "+exc, [exc, event]);
723 
724         var error = Firebug.Errors.reparseXPC(exc, spy.context);
725         if (error)
726         {
727             // TODO attach trace
728             if (FBTrace.DBG_ERRORS)
729                 FBTrace.sysout("spy.onHTTPSpyReadyStateChange: reparseXPC", error);
730 
731             // Make sure the exception is displayed in both Firefox & Firebug console.
732             throw new Error(error.message, error.href, error.lineNo);
733         }
734     }
735 }
736 
737 // ************************************************************************************************
738 
739 /**
740  * @domplate Represents a template for XHRs logged in the Console panel. The body of the
741  * log (displayed when expanded) is rendered using {@link Firebug.NetMonitor.NetInfoBody}.
742  */
743 Firebug.Spy.XHR = domplate(Firebug.Rep,
744 /** @lends Firebug.Spy.XHR */
745 {
746     tag:
747         DIV({"class": "spyHead", _repObject: "$object"},
748             TABLE({"class": "spyHeadTable focusRow outerFocusRow", cellpadding: 0, cellspacing: 0,
749                 "role": "listitem", "aria-expanded": "false"},
750                 TBODY({"role": "presentation"},
751                     TR({"class": "spyRow"},
752                         TD({"class": "spyTitleCol spyCol", onclick: "$onToggleBody"},
753                             DIV({"class": "spyTitle"},
754                                 "$object|getCaption"
755                             ),
756                             DIV({"class": "spyFullTitle spyTitle"},
757                                 "$object|getFullUri"
758                             )
759                         ),
760                         TD({"class": "spyCol"},
761                             DIV({"class": "spyStatus"}, "$object|getStatus")
762                         ),
763                         TD({"class": "spyCol"},
764                             IMG({"class": "spyIcon", src: "blank.gif"})
765                         ),
766                         TD({"class": "spyCol"},
767                             SPAN({"class": "spyTime"})
768                         ),
769                         TD({"class": "spyCol"},
770                             TAG(FirebugReps.SourceLink.tag, {object: "$object.sourceLink"})
771                         )
772                     )
773                 )
774             )
775         ),
776 
777     getCaption: function(spy)
778     {
779         return spy.method.toUpperCase() + " " + cropString(spy.getURL(), 100);
780     },
781 
782     getFullUri: function(spy)
783     {
784         return spy.method.toUpperCase() + " " + spy.getURL();
785     },
786 
787     getStatus: function(spy)
788     {
789         var text = "";
790         if (spy.statusCode)
791             text += spy.statusCode + " ";
792 
793         if (spy.statusText)
794             return text += spy.statusText;
795 
796         return text;
797     },
798 
799     onToggleBody: function(event)
800     {
801         var target = event.currentTarget;
802         var logRow = getAncestorByClass(target, "logRow-spy");
803 
804         if (isLeftClick(event))
805         {
806             toggleClass(logRow, "opened");
807 
808             var spy = getChildByClass(logRow, "spyHead").repObject;
809             var spyHeadTable = getAncestorByClass(target, "spyHeadTable");
810 
811             if (hasClass(logRow, "opened"))
812             {
813                 updateHttpSpyInfo(spy);
814                 if (spyHeadTable)
815                     spyHeadTable.setAttribute('aria-expanded', 'true');
816             }
817             else
818             {
819                 var netInfoBox = getChildByClass(spy.logRow, "spyHead", "netInfoBody");
820                 dispatch(Firebug.NetMonitor.NetInfoBody.fbListeners, "destroyTabBody", [netInfoBox, spy]);
821                 if (spyHeadTable)
822                     spyHeadTable.setAttribute('aria-expanded', 'false');
823             }
824         }
825     },
826 
827     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
828 
829     copyURL: function(spy)
830     {
831         copyToClipboard(spy.getURL());
832     },
833 
834     copyParams: function(spy)
835     {
836         var text = spy.postText;
837         if (!text)
838             return;
839 
840         var url = reEncodeURL(spy, text, true);
841         copyToClipboard(url);
842     },
843 
844     copyResponse: function(spy)
845     {
846         copyToClipboard(spy.responseText);
847     },
848 
849     openInTab: function(spy)
850     {
851         openNewTab(spy.getURL(), spy.postText);
852     },
853 
854     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
855 
856     supportsObject: function(object)
857     {
858         return object instanceof Firebug.Spy.XMLHttpRequestSpy;
859     },
860 
861     browseObject: function(spy, context)
862     {
863         var url = spy.getURL();
864         openNewTab(url);
865         return true;
866     },
867 
868     getRealObject: function(spy, context)
869     {
870         return spy.xhrRequest;
871     },
872 
873     getContextMenuItems: function(spy)
874     {
875         var items = [
876             {label: "CopyLocation", command: bindFixed(this.copyURL, this, spy) }
877         ];
878 
879         if (spy.postText)
880         {
881             items.push(
882                 {label: "CopyLocationParameters", command: bindFixed(this.copyParams, this, spy) }
883             );
884         }
885 
886         items.push(
887             {label: "CopyResponse", command: bindFixed(this.copyResponse, this, spy) },
888             "-",
889             {label: "OpenInTab", command: bindFixed(this.openInTab, this, spy) }
890         );
891 
892         return items;
893     }
894 });
895 
896 // ************************************************************************************************
897 
898 Firebug.XHRSpyListener =
899 {
900     onStart: function(context, spy)
901     {
902     },
903 
904     onLoad: function(context, spy)
905     {
906     }
907 };
908 
909 // ************************************************************************************************
910 
911 function updateTime(spy)
912 {
913     var timeBox = spy.logRow.getElementsByClassName("spyTime").item(0);
914     if (spy.responseTime)
915         timeBox.textContent = " " + formatTime(spy.responseTime);
916 }
917 
918 function updateLogRow(spy)
919 {
920     updateTime(spy);
921 
922     var statusBox = spy.logRow.getElementsByClassName("spyStatus").item(0);
923     statusBox.textContent = Firebug.Spy.XHR.getStatus(spy);
924 
925     removeClass(spy.logRow, "loading");
926     setClass(spy.logRow, "loaded");
927 
928     try
929     {
930         var errorRange = Math.floor(spy.xhrRequest.status/100);
931         if (errorRange == 4 || errorRange == 5)
932             setClass(spy.logRow, "error");
933     }
934     catch (exc)
935     {
936     }
937 }
938 
939 function updateHttpSpyInfo(spy)
940 {
941     if (!spy.logRow || !hasClass(spy.logRow, "opened"))
942         return;
943 
944     if (!spy.params)
945         spy.params = parseURLParams(spy.href+"");
946 
947     if (!spy.requestHeaders)
948         spy.requestHeaders = getRequestHeaders(spy);
949 
950     if (!spy.responseHeaders && spy.loaded)
951         spy.responseHeaders = getResponseHeaders(spy);
952 
953     var template = Firebug.NetMonitor.NetInfoBody;
954     var netInfoBox = getChildByClass(spy.logRow, "spyHead", "netInfoBody");
955     if (!netInfoBox)
956     {
957         var head = getChildByClass(spy.logRow, "spyHead");
958         netInfoBox = template.tag.append({"file": spy}, head);
959         dispatch(template.fbListeners, "initTabBody", [netInfoBox, spy]);
960         template.selectTabByName(netInfoBox, "Response");
961     }
962     else
963     {
964         template.updateInfo(netInfoBox, spy, spy.context);
965     }
966 }
967 
968 // ************************************************************************************************
969 
970 function getRequestHeaders(spy)
971 {
972     var headers = [];
973 
974     var channel = spy.xhrRequest.channel;
975     if (channel instanceof Ci.nsIHttpChannel)
976     {
977         channel.visitRequestHeaders({
978             visitHeader: function(name, value)
979             {
980                 headers.push({name: name, value: value});
981             }
982         });
983     }
984 
985     return headers;
986 }
987 
988 function getResponseHeaders(spy)
989 {
990     var headers = [];
991 
992     try
993     {
994         var channel = spy.xhrRequest.channel;
995         if (channel instanceof Ci.nsIHttpChannel)
996         {
997             channel.visitResponseHeaders({
998                 visitHeader: function(name, value)
999                 {
1000                     headers.push({name: name, value: value});
1001                 }
1002             });
1003         }
1004     }
1005     catch (exc)
1006     {
1007         if (FBTrace.DBG_SPY || FBTrace.DBG_ERRORS)
1008             FBTrace.sysout("spy.getResponseHeaders; EXCEPTION " +
1009                 safeGetRequestName(spy.request), exc);
1010     }
1011 
1012     return headers;
1013 }
1014 
1015 // ************************************************************************************************
1016 // Tracing Listener
1017 
1018 Firebug.Spy.TraceListener =
1019 {
1020     onDump: function(message)
1021     {
1022         var prefix = "spy.";
1023         var index = message.text.indexOf(prefix);
1024         if (index == 0)
1025         {
1026             message.text = message.text.substr(prefix.length);
1027             message.text = trim(message.text);
1028             message.type = "DBG_SPY";
1029         }
1030     }
1031 };
1032 
1033 // ************************************************************************************************
1034 // Registration
1035 
1036 Firebug.registerModule(Firebug.Spy);
1037 Firebug.registerRep(Firebug.Spy.XHR);
1038 
1039 // ************************************************************************************************
1040 }});
1041