1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/domplate",
  7     "firebug/chrome/reps",
  8     "firebug/lib/events",
  9     "firebug/net/requestObserver",
 10     "firebug/js/stackFrame",
 11     "firebug/lib/http",
 12     "firebug/lib/css",
 13     "firebug/lib/dom",
 14     "firebug/chrome/window",
 15     "firebug/lib/system",
 16     "firebug/lib/string",
 17     "firebug/lib/url",
 18     "firebug/lib/array",
 19     "firebug/trace/debug",
 20     "firebug/net/httpActivityObserver",
 21     "firebug/net/netUtils",
 22     "firebug/trace/traceListener",
 23     "firebug/trace/traceModule",
 24     "firebug/lib/wrapper",
 25     "firebug/lib/xpcom",
 26     "firebug/net/netPanel",
 27     "firebug/console/errors",
 28 ],
 29 function(Obj, Firebug, Domplate, FirebugReps, Events, HttpRequestObserver, StackFrame,
 30     Http, Css, Dom, Win, System, Str, Url, Arr, Debug, NetHttpActivityObserver, NetUtils,
 31     TraceListener, TraceModule, Wrapper, Xpcom) {
 32 
 33 // ********************************************************************************************* //
 34 // Constants
 35 
 36 const Cc = Components.classes;
 37 const Ci = Components.interfaces;
 38 
 39 var eventListenerService = Cc["@mozilla.org/eventlistenerservice;1"].
 40     getService(Ci.nsIEventListenerService);
 41 
 42 // List of contexts with XHR spy attached.
 43 var contexts = [];
 44 
 45 var versionChecker = Xpcom.CCSV("@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator");
 46 var appInfo = Xpcom.CCSV("@mozilla.org/xre/app-info;1", "nsIXULAppInfo");
 47 var fx18 = versionChecker.compare(appInfo.version, "18") >= 0;
 48 
 49 // ********************************************************************************************* //
 50 // Spy Module
 51 
 52 /**
 53  * @module Represents a XHR Spy module. The main purpose of the XHR Spy feature is to monitor
 54  * XHR activity of the current page and create appropriate log into the Console panel.
 55  * This feature can be controlled by an option <i>Show XMLHttpRequests</i> (from within the
 56  * console panel).
 57  *
 58  * The module is responsible for attaching/detaching a HTTP Observers when Firebug is
 59  * activated/deactivated for a site.
 60  */
 61 Firebug.Spy = Obj.extend(Firebug.Module,
 62 /** @lends Firebug.Spy */
 63 {
 64     dispatchName: "spy",
 65 
 66     initialize: function()
 67     {
 68         this.traceListener = new TraceListener("spy.", "DBG_SPY", true);
 69         TraceModule.addListener(this.traceListener);
 70 
 71         Firebug.Module.initialize.apply(this, arguments);
 72     },
 73 
 74     shutdown: function()
 75     {
 76         Firebug.Module.shutdown.apply(this, arguments);
 77 
 78         TraceModule.removeListener(this.traceListener);
 79     },
 80 
 81     initContext: function(context)
 82     {
 83         context.spies = [];
 84 
 85         if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
 86             this.attachObserver(context, context.window);
 87 
 88         if (FBTrace.DBG_SPY)
 89             FBTrace.sysout("spy.initContext " + contexts.length + " ", context.getName());
 90     },
 91 
 92     destroyContext: function(context)
 93     {
 94         // For any spies that are in progress, remove our listeners so that they don't leak
 95         this.detachObserver(context, null);
 96 
 97         if (FBTrace.DBG_SPY && context.spies && context.spies.length)
 98         {
 99             FBTrace.sysout("spy.destroyContext; ERROR There are spies in progress ("
100                 + context.spies.length + ") " + context.getName());
101         }
102 
103         // Make sure that all Spies in progress are detached at this moment.
104         // Clone the array beforehand since the spy object is removed from the
105         // original array within detach.
106         var spies = context.spies ? Arr.cloneArray(context.spies) : [];
107         for (var i=0; i<spies.length; i++)
108             spies[i].detach(true);
109 
110         delete context.spies;
111 
112         SpyHttpActivityObserver.cleanUp(context.window);
113 
114         if (FBTrace.DBG_SPY)
115             FBTrace.sysout("spy.destroyContext " + contexts.length + " ", context.getName());
116     },
117 
118     watchWindow: function(context, win)
119     {
120         if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
121             this.attachObserver(context, win);
122     },
123 
124     unwatchWindow: function(context, win)
125     {
126         if (FBTrace.DBG_SPY)
127             FBTrace.sysout("spy.unwatchWindow; " + (context ? context.getName() : "no context"));
128 
129         try
130         {
131             // This make sure that the existing context is properly removed from "contexts" array.
132             this.detachObserver(context, win);
133 
134             SpyHttpActivityObserver.cleanUp(win);
135         }
136         catch (ex)
137         {
138             // Get exceptions here sometimes, so let's just ignore them
139             // since the window is going away anyhow
140             Debug.ERROR(ex);
141         }
142     },
143 
144     updateOption: function(name, value)
145     {
146         // XXXjjb Honza, if Console.isEnabled(context) false, then this can't be called,
147         // but somehow seems not correct
148 
149         // XHR Spy needs to be detached/attached when:
150         // 1) The Show XMLHttpRequests options is off/on
151         // 2) The Console panel is disabled/enabled
152         // See also issue 5109
153         if (name == "showXMLHttpRequests" || name == "console.enableSites")
154         {
155             var tach = value ? this.attachObserver : this.detachObserver;
156 
157             Firebug.connection.eachContext(function tachAll(context)
158             {
159                 Win.iterateWindows(context.window, function(win)
160                 {
161                     tach.apply(this, [context, win]);
162                 });
163             });
164         }
165     },
166 
167     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
168     // Attaching Spy to XHR requests.
169 
170     /**
171      * Returns false if Spy should not be attached to XHRs executed by the specified window.
172      */
173     skipSpy: function(win)
174     {
175         if (!win)
176             return true;
177 
178         // Don't attach spy to chrome.
179         var uri = Win.safeGetWindowLocation(win);
180         if (uri && (Str.hasPrefix(uri, "about:") || Str.hasPrefix(uri, "chrome:")))
181             return true;
182     },
183 
184     attachObserver: function(context, win)
185     {
186         if (Firebug.Spy.skipSpy(win))
187             return;
188 
189         for (var i=0; i<contexts.length; ++i)
190         {
191             if ((contexts[i].context == context) && (contexts[i].win == win))
192                 return;
193         }
194 
195         // Register HTTP observers only once.
196         if (contexts.length == 0)
197         {
198             HttpRequestObserver.addObserver(SpyHttpObserver, "firebug-http-event", false);
199             SpyHttpActivityObserver.registerObserver();
200         }
201 
202         contexts.push({context: context, win: win});
203 
204         if (FBTrace.DBG_SPY)
205             FBTrace.sysout("spy.attachObserver (HTTP) " + contexts.length + " ", context.getName());
206     },
207 
208     detachObserver: function(context, win)
209     {
210         for (var i=0; i<contexts.length; ++i)
211         {
212             if (contexts[i].context == context)
213             {
214                 if (win && (contexts[i].win != win))
215                     continue;
216 
217                 contexts.splice(i, 1);
218 
219                 // If no context is using spy, remvove the (only one) HTTP observer.
220                 if (contexts.length == 0)
221                 {
222                     HttpRequestObserver.removeObserver(SpyHttpObserver, "firebug-http-event");
223                     SpyHttpActivityObserver.unregisterObserver();
224                 }
225 
226                 if (FBTrace.DBG_SPY)
227                     FBTrace.sysout("spy.detachObserver (HTTP) " + contexts.length + " ",
228                         context.getName());
229                 return;
230             }
231         }
232     },
233 
234     /**
235      * Return XHR object that is associated with specified request <i>nsIHttpChannel</i>.
236      * Returns null if the request doesn't represent XHR.
237      */
238     getXHR: function(request)
239     {
240         // Does also query-interface for nsIHttpChannel.
241         if (!(request instanceof Ci.nsIHttpChannel))
242             return null;
243 
244         try
245         {
246             var callbacks = request.notificationCallbacks;
247             if (callbacks)
248             {
249                 StackFrame.suspendShowStackTrace();
250                 return callbacks.getInterface(Ci.nsIXMLHttpRequest);
251             }
252         }
253         catch (exc)
254         {
255             if (exc.name == "NS_NOINTERFACE")
256             {
257                 if (FBTrace.DBG_SPY)
258                     FBTrace.sysout("spy.getXHR; Request is not nsIXMLHttpRequest: " +
259                         Http.safeGetRequestName(request));
260             }
261         }
262         finally
263         {
264             StackFrame.resumeShowStackTrace();
265         }
266 
267        return null;
268     },
269 });
270 
271 // ********************************************************************************************* //
272 
273 /**
274  * @class This observer uses {@link HttpRequestObserver} to monitor start and end of all XHRs.
275  * using <code>http-on-modify-request</code>, <code>http-on-examine-response</code> and
276  * <code>http-on-examine-cached-response</code> events. For every monitored XHR a new
277  * instance of {@link Firebug.Spy.XMLHttpRequestSpy} object is created. This instance is removed
278  * when the XHR is finished.
279  */
280 var SpyHttpObserver =
281 /** @lends SpyHttpObserver */
282 {
283     dispatchName: "SpyHttpObserver",
284 
285     observe: function(request, topic, data)
286     {
287         try
288         {
289             if ((topic == "http-on-modify-request" && !fx18) ||
290                 topic == "http-on-opening-request" ||
291                 topic == "http-on-examine-response" ||
292                 topic == "http-on-examine-cached-response")
293             {
294                 this.observeRequest(request, topic);
295             }
296         }
297         catch (exc)
298         {
299             if (FBTrace.DBG_ERRORS || FBTrace.DBG_SPY)
300                 FBTrace.sysout("spy.SpyHttpObserver EXCEPTION", exc);
301         }
302     },
303 
304     observeRequest: function(request, topic)
305     {
306         var win = Http.getWindowForRequest(request);
307         var xhr = Firebug.Spy.getXHR(request);
308 
309         // The request must be associated with window (i.e. tab) and it also must be
310         // real XHR request.
311         if (!win || !xhr)
312             return;
313 
314         for (var i=0; i<contexts.length; ++i)
315         {
316             var context = contexts[i];
317             if (context.win == win)
318             {
319                 var spyContext = context.context;
320                 var requestName = request.URI.asciiSpec;
321                 var requestMethod = request.requestMethod;
322 
323                 if (topic == "http-on-modify-request" && !fx18)
324                     this.requestStarted(request, xhr, spyContext, requestMethod, requestName);
325                 if (topic == "http-on-opening-request")
326                     this.requestStarted(request, xhr, spyContext, requestMethod, requestName);
327                 else if (topic == "http-on-examine-response")
328                     this.requestStopped(request, xhr, spyContext, requestMethod, requestName);
329                 else if (topic == "http-on-examine-cached-response")
330                     this.requestStopped(request, xhr, spyContext, requestMethod, requestName);
331 
332                 return;
333             }
334         }
335     },
336 
337     requestStarted: function(request, xhr, context, method, url)
338     {
339         var spy = getSpyForXHR(request, xhr, context);
340         spy.method = method;
341         spy.href = url;
342 
343         if (FBTrace.DBG_SPY)
344             FBTrace.sysout("spy.requestStarted; " + spy.href);
345 
346         // Get "body" for POST and PUT requests. It will be displayed in
347         // appropriate tab of the XHR.
348         if (method == "POST" || method == "PUT")
349             spy.postText = Http.readPostTextFromRequest(request, context);
350 
351         spy.urlParams = Url.parseURLParams(spy.href);
352 
353         // In case of redirects there is no stack and the source link is null.
354         spy.sourceLink = StackFrame.getStackSourceLink();
355 
356         if (!spy.requestHeaders)
357             spy.requestHeaders = getRequestHeaders(spy);
358 
359         // If it's enabled log the request into the console tab.
360         if (Firebug.showXMLHttpRequests && Firebug.Console.isAlwaysEnabled())
361         {
362             spy.logRow = Firebug.Console.log(spy, spy.context, "spy", null, true);
363             Css.setClass(spy.logRow, "loading");
364         }
365 
366         // Notify registered listeners. The onStart event is fired once for entire XHR
367         // (even if there is more redirects within the process).
368         var name = request.URI.asciiSpec;
369         var origName = request.originalURI.asciiSpec;
370         if (name == origName)
371             Events.dispatch(Firebug.Spy.fbListeners, "onStart", [context, spy]);
372 
373         // Remember the start time et the end, so it's most accurate.
374         spy.sendTime = new Date().getTime();
375     },
376 
377     requestStopped: function(request, xhr, context, method, url)
378     {
379         var spy = getSpyForXHR(request, xhr, context);
380         if (!spy)
381             return;
382 
383         spy.endTime = new Date().getTime();
384         spy.responseTime = spy.endTime - spy.sendTime;
385         spy.mimeType = NetUtils.getMimeType(request.contentType, request.name);
386 
387         if (!spy.responseHeaders)
388             spy.responseHeaders = getResponseHeaders(spy);
389 
390         if (!spy.statusText)
391         {
392             try
393             {
394                 spy.statusCode = request.responseStatus;
395                 spy.statusText = request.responseStatusText;
396             }
397             catch (exc)
398             {
399                 if (FBTrace.DBG_SPY)
400                     FBTrace.sysout("spy.requestStopped " + spy.href + ", status access FAILED", exc);
401             }
402         }
403 
404         if (spy.logRow)
405         {
406             updateLogRow(spy);
407             updateHttpSpyInfo(spy);
408         }
409 
410         // Remove only the Spy object that has been created for an intermediate rediret
411         // request. These exist only to be also displayed in the console and they
412         // don't attach any listeners to the original XHR object (which is always created
413         // only once even in case of redirects).
414         // xxxHonza: These requests are not observer by the activityObserver now
415         // (if they should be observed we have to remove them in the activityObserver)
416         if (!spy.onLoad && spy.context.spies)
417             Arr.remove(spy.context.spies, spy);
418 
419         if (FBTrace.DBG_SPY)
420         {
421             FBTrace.sysout("spy.requestStopped: " + spy.href + ", responseTime: " +
422                 spy.responseTime + "ms, spy.responseText: " +
423                 (spy.reponseText ? spy.responseText.length : 0) + " bytes");
424         }
425     }
426 };
427 
428 // ************************************************************************************************
429 // Activity Observer
430 
431 /**
432  * @class This observer is used to properly monitor even multipart XHRs. It's based on
433  * an activity-observer component that has been introduced in Firefox 3.6.
434  */
435 var SpyHttpActivityObserver = Obj.extend(NetHttpActivityObserver,
436 /** @lends SpyHttpActivityObserver */
437 {
438     dispatchName: "SpyHttpActivityObserver",
439     activeRequests: [],
440 
441     observeRequest: function(request, activityType, activitySubtype, timestamp,
442         extraSizeData, extraStringData)
443     {
444         if (activityType != Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION &&
445            (activityType == Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_SOCKET_TRANSPORT &&
446             activitySubtype != Ci.nsISocketTransport.STATUS_RECEIVING_FROM))
447             return;
448 
449         // xxxHonza: this code is duplicated in net.js, it should be refactored.
450         var win = Http.getWindowForRequest(request);
451         if (!win)
452         {
453             var index = this.activeRequests.indexOf(request);
454             if (index == -1)
455                 return;
456 
457             if (!(win = this.activeRequests[index+1]))
458                 return;
459         }
460 
461         for (var i=0; i<contexts.length; ++i)
462         {
463             var context = contexts[i];
464             if (context.win == win)
465             {
466                 var spyContext = context.context;
467                 var spy = getSpyForXHR(request, null, spyContext, true);
468                 if (spy)
469                     this.observeXHRActivity(win, spy, request, activitySubtype, timestamp);
470                 return;
471             }
472         }
473     },
474 
475     observeXHRActivity: function(win, spy, request, activitySubtype, timestamp)
476     {
477         // Activity observer has precise time info; use it.
478         var time = new Date();
479         time.setTime(timestamp/1000);
480 
481         if (activitySubtype == Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_REQUEST_HEADER)
482         {
483             if (FBTrace.DBG_SPY)
484                 FBTrace.sysout("spy.observeXHRActivity REQUEST_HEADER " +
485                     Http.safeGetRequestName(request));
486 
487             this.activeRequests.push(request);
488             this.activeRequests.push(win);
489 
490             spy.sendTime = time;
491             spy.transactionStarted = true;
492         }
493         else if (activitySubtype == Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
494         {
495             if (FBTrace.DBG_SPY)
496                 FBTrace.sysout("spy.observeXHRActivity TRANSACTION_CLOSE " +
497                     Http.safeGetRequestName(request));
498 
499             var index = this.activeRequests.indexOf(request);
500             this.activeRequests.splice(index, 2);
501 
502             spy.endTime = time;
503             spy.transactionClosed = true;
504 
505             // This should be the proper time to detach the Spy object, but only
506             // in the case when the XHR is already loaded. If the XHR is made as part of the
507             // page load, it may happen that the event (readyState == 4) comes later
508             // than actual TRANSACTION_CLOSE.
509             if (spy.loaded)
510                 spy.detach(false);
511         }
512         else if (activitySubtype == Ci.nsISocketTransport.STATUS_RECEIVING_FROM)
513         {
514             spy.endTime = time;
515         }
516     },
517 
518     cleanUp: function(win)
519     {
520         // https://bugzilla.mozilla.org/show_bug.cgi?id=669730
521         for (var i=0; i<this.activeRequests.length; i+=2)
522         {
523             if (this.activeRequests[i+1] == win)
524             {
525                 this.activeRequests.splice(i, 2);
526                 i -= 2;
527             }
528         }
529     }
530 });
531 
532 // ********************************************************************************************* //
533 
534 function getSpyForXHR(request, xhrRequest, context, noCreate)
535 {
536     var spy = null;
537 
538     if (!context.spies)
539     {
540         if (FBTrace.DBG_ERRORS)
541         {
542             FBTrace.sysout("spy.getSpyForXHR; ERROR no spies array " +
543                 Http.safeGetRequestName(request));
544         }
545         return;
546     }
547 
548     // Iterate all existing spy objects in this context and look for one that is
549     // already created for this request.
550     var length = context.spies.length;
551     for (var i=0; i<length; i++)
552     {
553         spy = context.spies[i];
554         if (spy.request == request)
555             return spy;
556     }
557 
558     if (noCreate)
559         return null;
560 
561     spy = new Firebug.Spy.XMLHttpRequestSpy(request, xhrRequest, context);
562     context.spies.push(spy);
563 
564     var name = request.URI.asciiSpec;
565     var origName = request.originalURI.asciiSpec;
566 
567     // Attach spy only to the original request. Notice that there can be more network requests
568     // made by the same XHR if redirects are involved.
569     if (name == origName)
570         spy.attach();
571 
572     if (FBTrace.DBG_SPY)
573     {
574         FBTrace.sysout("spy.getSpyForXHR; New spy object created (" +
575             (name == origName ? "new XHR" : "redirected XHR") + ") for: " + name);
576     }
577 
578     return spy;
579 }
580 
581 // ********************************************************************************************* //
582 
583 /**
584  * @class This class represents a Spy object that is attached to XHR. This object
585  * registers various listeners into the XHR in order to monitor various events fired
586  * during the request process (onLoad, onAbort, etc.)
587  */
588 Firebug.Spy.XMLHttpRequestSpy = function(request, xhrRequest, context)
589 {
590     this.request = request;
591     this.xhrRequest = xhrRequest;
592     this.context = context;
593     this.responseText = "";
594 
595     // For compatibility with the Net templates.
596     this.isXHR = true;
597 
598     // Support for activity-observer
599     this.transactionStarted = false;
600     this.transactionClosed = false;
601 };
602 
603 Firebug.Spy.XMLHttpRequestSpy.prototype =
604 /** @lends Firebug.Spy.XMLHttpRequestSpy */
605 {
606     attach: function()
607     {
608         var spy = this;
609 
610         this.onReadyStateChange = function(event) { onHTTPSpyReadyStateChange(spy, event); };
611         this.onLoad = function() { onHTTPSpyLoad(spy); };
612         this.onError = function() { onHTTPSpyError(spy); };
613         this.onAbort = function() { onHTTPSpyAbort(spy); };
614 
615         this.onEventListener = function(event)
616         {
617             switch (event.type)
618             {
619                 case "readystatechange":
620                     onHTTPSpyReadyStateChange(spy, event);
621                 break;
622                 case "load":
623                     onHTTPSpyLoad(spy);
624                 break;
625                 case "error":
626                     onHTTPSpyError(spy);
627                 break;
628                 case "abort":
629                     onHTTPSpyAbort(spy);
630                 break;
631             }
632         };
633 
634         if (typeof(eventListenerService.addListenerForAllEvents) == "function")
635         {
636             eventListenerService.addListenerForAllEvents(this.xhrRequest,
637                 this.onEventListener, true, false, false);
638         }
639         else
640         {
641             this.onreadystatechange = this.xhrRequest.onreadystatechange;
642             this.xhrRequest.onreadystatechange = this.onReadyStateChange;
643 
644             this.xhrRequest.addEventListener("load", this.onLoad, false);
645             this.xhrRequest.addEventListener("error", this.onError, false);
646             this.xhrRequest.addEventListener("abort", this.onAbort, false);
647         }
648 
649         if (FBTrace.DBG_SPY)
650             FBTrace.sysout("spy.attach; " + Http.safeGetRequestName(this.request));
651     },
652 
653     detach: function(force)
654     {
655         // Bubble out if already detached.
656         if (!this.onEventListener)
657             return;
658 
659         // If the activity distributor is available, let's detach it when the XHR
660         // transaction is closed. Since, in case of multipart XHRs the onLoad method
661         // (readyState == 4) can be called mutliple times.
662         // Keep in mind:
663         // 1) It can happen that the TRANSACTION_CLOSE event comes before onload (if
664         // the XHR is made as part of the page load), so detach if it's already closed.
665         // 2) In case of immediate cache responses, the transaction doesn't have to
666         // be started at all (or the activity observer is not available in Firefox 3.5).
667         // So, also detach in this case.
668         // Make sure spy will detach if force is true.
669         if (!force && this.transactionStarted && !this.transactionClosed)
670             return;
671 
672         if (FBTrace.DBG_SPY)
673             FBTrace.sysout("spy.detach; " + this.href);
674 
675         // Remove itself from the list of active spies.
676         Arr.remove(this.context.spies, this);
677 
678         if (typeof(eventListenerService.addListenerForAllEvents) == "function")
679         {
680             eventListenerService.removeListenerForAllEvents(this.xhrRequest,
681                 this.onEventListener, true, false);
682         }
683         else
684         {
685             if (this.onreadystatechange)
686                 this.xhrRequest.onreadystatechange = this.onreadystatechange;
687 
688             try { this.xhrRequest.removeEventListener("load", this.onLoad, false); } catch (e) {}
689             try { this.xhrRequest.removeEventListener("error", this.onError, false); } catch (e) {}
690             try { this.xhrRequest.removeEventListener("abort", this.onAbort, false); } catch (e) {}
691         }
692 
693         this.onreadystatechange = null;
694         this.onLoad = null;
695         this.onError = null;
696         this.onAbort = null;
697 
698         this.onEventListener = null;
699     },
700 
701     getURL: function()
702     {
703         // Don't use this.xhrRequest.channel.name to get the URL. In cases where the
704         // same XHR object is reused for more requests, the URL can be wrong (issue 4738).
705         return this.href;
706     },
707 
708     // Cache listener
709     onStopRequest: function(context, request, responseText)
710     {
711         if (FBTrace.DBG_SPY)
712             FBTrace.sysout("spy.onStopRequest: " + Http.safeGetRequestName(request));
713 
714         if (!responseText)
715             return;
716 
717         if (request == this.request)
718             this.responseText = responseText;
719     },
720 };
721 
722 // ********************************************************************************************* //
723 
724 function onHTTPSpyReadyStateChange(spy, event)
725 {
726     if (FBTrace.DBG_SPY)
727     {
728         FBTrace.sysout("spy.onHTTPSpyReadyStateChange " + spy.xhrRequest.readyState +
729             " (multipart: " + spy.xhrRequest.multipart + ")");
730     }
731 
732     // Remember just in case spy is detached (readyState == 4).
733     var originalHandler = spy.onreadystatechange;
734 
735     // Force response text to be updated in the UI (in case the console entry
736     // has been already expanded and the response tab selected).
737     if (spy.logRow && spy.xhrRequest.readyState >= 3)
738     {
739         var netInfoBox = Dom.getChildByClass(spy.logRow, "spyHead", "netInfoBody");
740         if (netInfoBox)
741         {
742             netInfoBox.htmlPresented = false;
743             netInfoBox.responsePresented = false;
744         }
745     }
746 
747     // If the request is loading update the end time.
748     if (spy.logRow && spy.xhrRequest.readyState == 3 && spy.sendTime && spy.endTime)
749     {
750         spy.responseTime = spy.endTime - spy.sendTime;
751         updateTime(spy);
752     }
753 
754     // Request loaded. Get all the info from the request now, just in case the
755     // XHR would be aborted in the original onReadyStateChange handler.
756     if (spy.xhrRequest.readyState == 4)
757     {
758         // Cumulate response so that multipart response content is properly displayed.
759         spy.responseText += Http.safeGetXHRResponseText(spy.xhrRequest);
760 
761         // The XHR is loaded now (used also by the activity observer).
762         spy.loaded = true;
763 
764         // Update UI.
765         updateLogRow(spy);
766         updateHttpSpyInfo(spy, true);
767 
768         // Notify the Net panel about a request being loaded.
769         // xxxHonza: I don't think this is necessary.
770         var netProgress = spy.context.netProgress;
771         if (netProgress)
772             netProgress.post(netProgress.stopFile, [spy.request, spy.endTime, spy.postText,
773                 spy.responseText]);
774 
775         // Notify registered listeners about finish of the XHR.
776         Events.dispatch(Firebug.Spy.fbListeners, "onLoad", [spy.context, spy]);
777     }
778 
779     // Pass the event to the original page handler.
780     if (typeof(eventListenerService.addListenerForAllEvents) == "undefined")
781         callPageHandler(spy, event, originalHandler);
782 }
783 
784 function callPageHandler(spy, event, originalHandler)
785 {
786     try
787     {
788         // Calling the page handler throwed an exception (see #502959)
789         // This should be fixed in Firefox 3.5
790         if (originalHandler && event)
791         {
792             if (originalHandler.handleEvent)
793                 originalHandler.handleEvent(event);
794             else
795                 originalHandler.call(spy.xhrRequest, event);
796         }
797     }
798     catch (exc)
799     {
800         if (FBTrace.DBG_ERRORS)
801             FBTrace.sysout("spy.onHTTPSpyReadyStateChange: EXCEPTION " + exc, [exc, event]);
802 
803         var xpcError = Firebug.Errors.reparseXPC(exc, spy.context);
804         if (xpcError)
805         {
806             // TODO attach trace
807             if (FBTrace.DBG_ERRORS)
808                 FBTrace.sysout("spy.onHTTPSpyReadyStateChange: reparseXPC", xpcError);
809 
810             // Make sure the exception is displayed in both Firefox & Firebug console.
811             throw new Error(xpcError.message, xpcError.href, xpcError.lineNo);
812         }
813         else
814         {
815             throw exc;
816         }
817     }
818 }
819 
820 function onHTTPSpyLoad(spy)
821 {
822     if (FBTrace.DBG_SPY)
823         FBTrace.sysout("spy.onHTTPSpyLoad: " + spy.href);
824 
825     // Detach must be done in onLoad (not in onreadystatechange) otherwise
826     // onAbort would not be handled.
827     spy.detach(false);
828 
829     // If the spy is not loaded yet (and so, the response was not cached), do it now.
830     // This can happen since synchronous XHRs don't fire onReadyStateChange event (issue 2868).
831     if (!spy.loaded)
832     {
833         spy.loaded = true;
834         spy.responseText = Http.safeGetXHRResponseText(spy.xhrRequest);
835 
836         updateLogRow(spy);
837         updateHttpSpyInfo(spy, true);
838     }
839 }
840 
841 function onHTTPSpyError(spy)
842 {
843     if (FBTrace.DBG_SPY)
844         FBTrace.sysout("spy.onHTTPSpyError; " + spy.href);
845 
846     spy.detach(false);
847     spy.loaded = true;
848     spy.error= true;
849 
850     updateLogRow(spy);
851 }
852 
853 function onHTTPSpyAbort(spy)
854 {
855     if (FBTrace.DBG_SPY)
856         FBTrace.sysout("spy.onHTTPSpyAbort: " + spy.href);
857 
858     spy.detach(false);
859     spy.loaded = true;
860 
861     // Ignore aborts if the request already has a response status.
862     if (spy.xhrRequest.status)
863     {
864         updateLogRow(spy);
865         return;
866     }
867 
868     spy.aborted = true;
869     spy.statusText = "Aborted";
870 
871     updateLogRow(spy);
872 
873     // Notify Net pane about a request beeing aborted.
874     // xxxHonza: the net panel shoud find out this itself.
875     var netProgress = spy.context.netProgress;
876     if (netProgress)
877     {
878         netProgress.post(netProgress.abortFile, [spy.request, spy.endTime, spy.postText,
879             spy.responseText]);
880     }
881 }
882 
883 // ********************************************************************************************* //
884 
885 /**
886  * @domplate Represents a template for XHRs logged in the Console panel. The body of the
887  * log (displayed when expanded) is rendered using {@link Firebug.NetMonitor.NetInfoBody}.
888  */
889 with (Domplate) {
890 Firebug.Spy.XHR = domplate(Firebug.Rep,
891 /** @lends Firebug.Spy.XHR */
892 {
893     tag:
894         DIV({"class": "spyHead", _repObject: "$object"},
895             TABLE({"class": "spyHeadTable focusRow outerFocusRow", cellpadding: 0, cellspacing: 0,
896                 "role": "listitem", "aria-expanded": "false"},
897                 TBODY({"role": "presentation"},
898                     TR({"class": "spyRow"},
899                         TD({"class": "spyTitleCol spyCol", onclick: "$onToggleBody"},
900                             DIV({"class": "spyTitle"},
901                                 "$object|getCaption"
902                             ),
903                             DIV({"class": "spyFullTitle spyTitle"},
904                                 "$object|getFullUri"
905                             )
906                         ),
907                         TD({"class": "spyCol"},
908                             DIV({"class": "spyStatus"}, "$object|getStatus")
909                         ),
910                         TD({"class": "spyCol"},
911                             IMG({"class": "spyIcon", src: "blank.gif"})
912                         ),
913                         TD({"class": "spyCol"},
914                             SPAN({"class": "spyTime"})
915                         ),
916                         TD({"class": "spyCol"},
917                             TAG(FirebugReps.SourceLink.tag, {object: "$object.sourceLink"})
918                         )
919                     )
920                 )
921             )
922         ),
923 
924     getCaption: function(spy)
925     {
926         return spy.method.toUpperCase() + " " + Str.cropString(spy.getURL(), 100);
927     },
928 
929     getFullUri: function(spy)
930     {
931         return spy.method.toUpperCase() + " " + spy.getURL();
932     },
933 
934     getStatus: function(spy)
935     {
936         var text = "";
937         if (spy.statusCode)
938             text += spy.statusCode + " ";
939 
940         if (spy.statusText)
941             return text += spy.statusText;
942 
943         return text;
944     },
945 
946     onToggleBody: function(event)
947     {
948         var target = event.currentTarget;
949         var logRow = Dom.getAncestorByClass(target, "logRow-spy");
950 
951         if (Events.isLeftClick(event))
952         {
953             Css.toggleClass(logRow, "opened");
954 
955             var spy = Dom.getChildByClass(logRow, "spyHead").repObject;
956             var spyHeadTable = Dom.getAncestorByClass(target, "spyHeadTable");
957 
958             if (Css.hasClass(logRow, "opened"))
959             {
960                 updateHttpSpyInfo(spy);
961 
962                 if (spyHeadTable)
963                     spyHeadTable.setAttribute("aria-expanded", "true");
964             }
965             else
966             {
967                 var netInfoBox = Dom.getChildByClass(spy.logRow, "spyHead", "netInfoBody");
968                 Events.dispatch(Firebug.NetMonitor.NetInfoBody.fbListeners, "destroyTabBody",
969                     [netInfoBox, spy]);
970 
971                 if (spyHeadTable)
972                     spyHeadTable.setAttribute("aria-expanded", "false");
973 
974                 // Remove the info box, it'll be re-created (together with custom tabs)
975                 // the next time the XHR entry is opened/updated.
976                 netInfoBox.parentNode.removeChild(netInfoBox);
977             }
978         }
979     },
980 
981     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
982 
983     copyURL: function(spy)
984     {
985         System.copyToClipboard(spy.getURL());
986     },
987 
988     copyParams: function(spy)
989     {
990         var text = spy.postText;
991         if (!text)
992             return;
993 
994         var url = Url.reEncodeURL(spy, text, true);
995         System.copyToClipboard(url);
996     },
997 
998     copyResponse: function(spy)
999     {
1000         System.copyToClipboard(spy.responseText);
1001     },
1002 
1003     openInTab: function(spy)
1004     {
1005         Win.openNewTab(spy.getURL(), spy.postText);
1006     },
1007 
1008     resend: function(spy, context)
1009     {
1010         try
1011         {
1012             // xxxHonza: must be done through Console RDP
1013             var win = Wrapper.unwrapObject(context.window);
1014             var request = new win.XMLHttpRequest();
1015             request.open(spy.method, spy.href, true);
1016 
1017             var headers = spy.requestHeaders;
1018             for (var i=0; headers && i<headers.length; i++)
1019             {
1020                 var header = headers[i];
1021                 request.setRequestHeader(header.name, header.value);
1022             }
1023 
1024             var postData = NetUtils.getPostText(spy, context, true);
1025             request.send(postData);
1026         }
1027         catch (err)
1028         {
1029             if (FBTrace.DBG_ERRORS)
1030                 FBTrace.sysout("spy.resend; EXCEPTION " + err, err);
1031         }
1032     },
1033 
1034     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1035 
1036     supportsObject: function(object, type)
1037     {
1038         return object instanceof Firebug.Spy.XMLHttpRequestSpy;
1039     },
1040 
1041     browseObject: function(spy, context)
1042     {
1043         var url = spy.getURL();
1044         Win.openNewTab(url);
1045         return true;
1046     },
1047 
1048     getRealObject: function(spy, context)
1049     {
1050         return spy.xhrRequest;
1051     },
1052 
1053     getContextMenuItems: function(spy, target, context)
1054     {
1055         var items = [{
1056             label: "CopyLocation",
1057             tooltiptext: "clipboard.tip.Copy_Location",
1058             id: "fbSpyCopyLocation",
1059             command: Obj.bindFixed(this.copyURL, this, spy)
1060         }];
1061 
1062         if (spy.postText)
1063         {
1064             items.push({
1065                 label: "CopyLocationParameters",
1066                 tooltiptext: "net.tip.Copy_Location_Parameters",
1067                 command: Obj.bindFixed(this.copyParams, this, spy)
1068             });
1069         }
1070 
1071         items.push({
1072             label: "CopyResponse",
1073             id: "fbSpyCopyResponse",
1074             command: Obj.bindFixed(this.copyResponse, this, spy)
1075         });
1076 
1077         items.push("-");
1078 
1079         items.push({
1080             label: "OpenInTab",
1081             tooltiptext: "firebug.tip.Open_In_Tab",
1082             id: "fbSpyOpenInTab",
1083             command: Obj.bindFixed(this.openInTab, this, spy)
1084         });
1085 
1086         items.push({
1087             label: "Open_Response_In_New_Tab",
1088             tooltiptext: "net.tip.Open_Response_In_New_Tab",
1089             id: "fbSpyOpenResponseInTab",
1090             command: Obj.bindFixed(NetUtils.openResponseInTab, this, spy)
1091         });
1092 
1093         items.push("-");
1094 
1095         items.push({
1096             label: "net.label.Resend",
1097             tooltiptext: "net.tip.Resend",
1098             id: "fbSpyResend",
1099             command: Obj.bindFixed(this.resend, this, spy, context)
1100         });
1101 
1102         return items;
1103     }
1104 })};
1105 
1106 // ********************************************************************************************* //
1107 
1108 Firebug.XHRSpyListener =
1109 {
1110     onStart: function(context, spy)
1111     {
1112     },
1113 
1114     onLoad: function(context, spy)
1115     {
1116     }
1117 };
1118 
1119 // ********************************************************************************************* //
1120 
1121 function updateTime(spy)
1122 {
1123     if(spy.logRow)
1124     {
1125         var timeBox = spy.logRow.getElementsByClassName("spyTime").item(0);
1126         if (spy.responseTime)
1127             timeBox.textContent = " " + Str.formatTime(spy.responseTime);
1128     }
1129 }
1130 
1131 function updateLogRow(spy)
1132 {
1133     updateTime(spy);
1134 
1135     if(spy.logRow)
1136     {
1137         var statusBox = spy.logRow.getElementsByClassName("spyStatus").item(0);
1138         statusBox.textContent = Firebug.Spy.XHR.getStatus(spy);
1139     }
1140 
1141     if (spy.loaded)
1142     {
1143         Css.removeClass(spy.logRow, "loading");
1144         Css.setClass(spy.logRow, "loaded");
1145     }
1146 
1147     if (spy.error || spy.aborted)
1148     {
1149         Css.setClass(spy.logRow, "error");
1150     }
1151 
1152     try
1153     {
1154         var errorRange = Math.floor(spy.xhrRequest.status/100);
1155         if (errorRange == 4 || errorRange == 5)
1156             Css.setClass(spy.logRow, "error");
1157     }
1158     catch (exc)
1159     {
1160     }
1161 }
1162 
1163 function updateHttpSpyInfo(spy, updateInfoBody)
1164 {
1165     if (!spy.logRow || !Css.hasClass(spy.logRow, "opened"))
1166         return;
1167 
1168     if (!spy.params)
1169         spy.params = Url.parseURLParams(spy.href + "");
1170 
1171     if (!spy.requestHeaders)
1172         spy.requestHeaders = getRequestHeaders(spy);
1173 
1174     if (!spy.responseHeaders && spy.loaded)
1175         spy.responseHeaders = getResponseHeaders(spy);
1176 
1177     var template = Firebug.NetMonitor.NetInfoBody;
1178     var netInfoBox = Dom.getChildByClass(spy.logRow, "spyHead", "netInfoBody");
1179 
1180     var defaultTab;
1181 
1182     // If the associated XHR row is currently expanded, make sure to recreate
1183     // the info bodies if the flag says so.
1184     if (updateInfoBody)
1185     {
1186         // Remember the current selected info tab.
1187         if (netInfoBox.selectedTab)
1188             defaultTab = netInfoBox.selectedTab.getAttribute("view");
1189 
1190         // Remove the info box so, it's recreated below.
1191         netInfoBox.parentNode.removeChild(netInfoBox);
1192         netInfoBox = null;
1193     }
1194 
1195     if (!netInfoBox)
1196     {
1197         var head = Dom.getChildByClass(spy.logRow, "spyHead");
1198         netInfoBox = template.tag.append({"file": spy}, head);
1199 
1200         // Notify listeners so, custom info tabs can be appended
1201         Events.dispatch(template.fbListeners, "initTabBody", [netInfoBox, spy]);
1202 
1203         // If the response tab isn't available/visible (perhaps the response didn't came yet),
1204         // select the 'Headers' tab by default or keep the default tab.
1205         defaultTab = defaultTab || (template.hideResponse(spy) ? "Headers" : "Response");
1206         template.selectTabByName(netInfoBox, defaultTab);
1207     }
1208     else
1209     {
1210         template.updateInfo(netInfoBox, spy, spy.context);
1211     }
1212 }
1213 
1214 // ********************************************************************************************* //
1215 
1216 function getRequestHeaders(spy)
1217 {
1218     var headers = [];
1219 
1220     var channel = spy.xhrRequest.channel;
1221     if (channel instanceof Ci.nsIHttpChannel)
1222     {
1223         channel.visitRequestHeaders(
1224         {
1225             visitHeader: function(name, value)
1226             {
1227                 headers.push({name: name, value: value});
1228             }
1229         });
1230     }
1231 
1232     return headers;
1233 }
1234 
1235 function getResponseHeaders(spy)
1236 {
1237     var headers = [];
1238 
1239     try
1240     {
1241         var channel = spy.xhrRequest.channel;
1242         if (channel instanceof Ci.nsIHttpChannel)
1243         {
1244             channel.visitResponseHeaders(
1245             {
1246                 visitHeader: function(name, value)
1247                 {
1248                     headers.push({name: name, value: value});
1249                 }
1250             });
1251         }
1252     }
1253     catch (exc)
1254     {
1255         if (FBTrace.DBG_SPY || FBTrace.DBG_ERRORS)
1256         {
1257             FBTrace.sysout("spy.getResponseHeaders; EXCEPTION " +
1258                 Http.safeGetRequestName(spy.request), exc);
1259         }
1260     }
1261 
1262     return headers;
1263 }
1264 
1265 // ********************************************************************************************* //
1266 // Registration
1267 
1268 Firebug.registerModule(Firebug.Spy);
1269 Firebug.registerRep(Firebug.Spy.XHR);
1270 
1271 return Firebug.Spy;
1272 
1273 // ********************************************************************************************* //
1274 });
1275