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