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