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 const Cr = Components.results;
 11 
 12 const CacheService = Cc["@mozilla.org/network/cache-service;1"];
 13 const ImgCache = Cc["@mozilla.org/image/cache;1"];
 14 const IOService = Cc["@mozilla.org/network/io-service;1"];
 15 const prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch2);
 16 
 17 const NOTIFY_ALL = Ci.nsIWebProgress.NOTIFY_ALL;
 18 
 19 const nsIWebProgressListener = Ci.nsIWebProgressListener;
 20 const STATE_IS_WINDOW = nsIWebProgressListener.STATE_IS_WINDOW;
 21 const STATE_IS_DOCUMENT = nsIWebProgressListener.STATE_IS_DOCUMENT;
 22 const STATE_IS_NETWORK = nsIWebProgressListener.STATE_IS_NETWORK;
 23 const STATE_IS_REQUEST = nsIWebProgressListener.STATE_IS_REQUEST;
 24 const STATE_START = nsIWebProgressListener.STATE_START;
 25 const STATE_STOP = nsIWebProgressListener.STATE_STOP;
 26 const STATE_TRANSFERRING = nsIWebProgressListener.STATE_TRANSFERRING;
 27 
 28 const LOAD_BACKGROUND = Ci.nsIRequest.LOAD_BACKGROUND;
 29 const LOAD_FROM_CACHE = Ci.nsIRequest.LOAD_FROM_CACHE;
 30 const LOAD_DOCUMENT_URI = Ci.nsIChannel.LOAD_DOCUMENT_URI;
 31 
 32 const NS_ERROR_CACHE_KEY_NOT_FOUND = 0x804B003D;
 33 const NS_ERROR_CACHE_WAIT_FOR_VALIDATION = 0x804B0040;
 34 
 35 var nsIHttpActivityObserver = Ci.nsIHttpActivityObserver;
 36 var nsIHttpActivityObserver = Ci.nsIHttpActivityObserver;
 37 var nsISocketTransport = Ci.nsISocketTransport;
 38 
 39 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 40 
 41 const reIgnore = /about:|javascript:|resource:|chrome:|jar:/;
 42 const reResponseStatus = /HTTP\/1\.\d\s(\d+)\s(.*)/;
 43 const layoutInterval = 300;
 44 const indentWidth = 18;
 45 
 46 var cacheSession = null;
 47 var contexts = new Array();
 48 var panelName = "net";
 49 var maxQueueRequests = 500;
 50 var panelBar1 = $("fbPanelBar1");
 51 var activeRequests = [];
 52 
 53 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 54 
 55 const mimeExtensionMap =
 56 {
 57     "txt": "text/plain",
 58     "html": "text/html",
 59     "htm": "text/html",
 60     "xhtml": "text/html",
 61     "xml": "text/xml",
 62     "css": "text/css",
 63     "js": "application/x-javascript",
 64     "jss": "application/x-javascript",
 65     "jpg": "image/jpg",
 66     "jpeg": "image/jpeg",
 67     "gif": "image/gif",
 68     "png": "image/png",
 69     "bmp": "image/bmp",
 70     "swf": "application/x-shockwave-flash",
 71     "flv": "video/x-flv"
 72 };
 73 
 74 const fileCategories =
 75 {
 76     "undefined": 1,
 77     "html": 1,
 78     "css": 1,
 79     "js": 1,
 80     "xhr": 1,
 81     "image": 1,
 82     "flash": 1,
 83     "txt": 1,
 84     "bin": 1
 85 };
 86 
 87 const textFileCategories =
 88 {
 89     "txt": 1,
 90     "html": 1,
 91     "xhr": 1,
 92     "css": 1,
 93     "js": 1
 94 };
 95 
 96 const binaryFileCategories =
 97 {
 98     "bin": 1,
 99     "flash": 1
100 };
101 
102 const mimeCategoryMap =
103 {
104     "text/plain": "txt",
105     "application/octet-stream": "bin",
106     "text/html": "html",
107     "text/xml": "html",
108     "application/xhtml+xml": "html",
109     "text/css": "css",
110     "application/x-javascript": "js",
111     "text/javascript": "js",
112     "application/javascript" : "js",
113     "image/jpeg": "image",
114     "image/jpg": "image",
115     "image/gif": "image",
116     "image/png": "image",
117     "image/bmp": "image",
118     "application/x-shockwave-flash": "flash",
119     "video/x-flv": "flash"
120 };
121 
122 const binaryCategoryMap =
123 {
124     "image": 1,
125     "flash" : 1
126 };
127 
128 // ************************************************************************************************
129 
130 /**
131  * @module Represents a module object for the Net panel. This object is derived
132  * from <code>Firebug.ActivableModule</code> in order to support activation (enable/disable).
133  * This allows to avoid (performance) expensive features if the functionality is not necessary
134  * for the user.
135  */
136 Firebug.NetMonitor = extend(Firebug.ActivableModule,
137 {
138     dispatchName: "netMonitor",
139     clear: function(context)
140     {
141         // The user pressed a Clear button so, remove content of the panel...
142         var panel = context.getPanel(panelName, true);
143         if (panel)
144             panel.clear();
145     },
146 
147     onToggleFilter: function(context, filterCategory)
148     {
149         if (!context.netProgress)
150             return;
151 
152         Firebug.setPref(Firebug.prefDomain, "netFilterCategory", filterCategory);
153 
154         // The content filter has been changed. Make sure that the content
155         // of the panel is updated (CSS is used to hide or show individual files).
156         var panel = context.getPanel(panelName, true);
157         if (panel)
158         {
159             panel.setFilter(filterCategory);
160             panel.updateSummaries(now(), true);
161         }
162     },
163 
164     syncFilterButtons: function(chrome)
165     {
166         var button = chrome.$("fbNetFilter-" + Firebug.netFilterCategory);
167         button.checked = true;
168     },
169 
170     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
171     // extends Module
172 
173     initializeUI: function()
174     {
175         Firebug.ActivableModule.initializeUI.apply(this, arguments);
176 
177         // Initialize max limit for logged requests.
178         NetLimit.updateMaxLimit();
179 
180         // Synchronize UI buttons with the current filter.
181         this.syncFilterButtons(FirebugChrome);
182 
183         prefs.addObserver(Firebug.prefDomain, NetLimit, false);
184     },
185 
186     initialize: function()
187     {
188         this.panelName = panelName;
189 
190         Firebug.ActivableModule.initialize.apply(this, arguments);
191 
192         if (Firebug.TraceModule)
193             Firebug.TraceModule.addListener(this.TraceListener);
194 
195         // HTTP observer must be registered now (and not in monitorContext, since if a
196         // page is opened in a new tab the top document request would be missed otherwise.
197         Firebug.NetMonitor.NetHttpObserver.registerObserver();
198         NetHttpActivityObserver.registerObserver();
199 
200         Firebug.Debugger.addListener(this.DebuggerListener);
201     },
202 
203     internationalizeUI: function(doc)
204     {
205         var element = doc.getElementById("fbNetPersist");
206         FBL.internationalize(element, "label");
207         FBL.internationalize(element, "tooltiptext");
208     },
209 
210     shutdown: function()
211     {
212         prefs.removeObserver(Firebug.prefDomain, this, false);
213         if (Firebug.TraceModule)
214             Firebug.TraceModule.removeListener(this.TraceListener);
215 
216         Firebug.NetMonitor.NetHttpObserver.unregisterObserver();
217         NetHttpActivityObserver.unregisterObserver();
218 
219         Firebug.Debugger.removeListener(this.DebuggerListener);
220     },
221 
222     initContext: function(context, persistedState)
223     {
224         Firebug.ActivableModule.initContext.apply(this, arguments);
225 
226         if (FBTrace.DBG_NET)
227             FBTrace.sysout("net.initContext for: " + context.getName());
228 
229         if (context.window && 'addEventListener' in context.window)
230         {
231             var window = context.window;
232 
233             // Register "load" listener in order to track window load time.
234             var onWindowLoadHandler = function() {
235                 if (context.netProgress)
236                     context.netProgress.post(windowLoad, [window, now()]);
237                 window.removeEventListener("load", onWindowLoadHandler, true);
238             }
239             window.addEventListener("load", onWindowLoadHandler, true);
240 
241             // Register "DOMContentLoaded" listener to track timing.
242             var onContentLoadHandler = function() {
243                 if (context.netProgress)
244                     context.netProgress.post(contentLoad, [window, now()]);
245                 window.removeEventListener("DOMContentLoaded", onContentLoadHandler, true);
246             }
247 
248             window.addEventListener("DOMContentLoaded", onContentLoadHandler, true);
249         }
250 
251         if (Firebug.NetMonitor.isAlwaysEnabled())
252             monitorContext(context);
253 
254         if (context.netProgress)
255         {
256             // Load existing breakpoints
257             var persistedPanelState = getPersistedState(context, panelName);
258             if (persistedPanelState.breakpoints)
259                 context.netProgress.breakpoints = persistedPanelState.breakpoints;
260         }
261     },
262 
263     reattachContext: function(browser, context)
264     {
265         Firebug.ActivableModule.reattachContext.apply(this, arguments);
266         this.syncFilterButtons(Firebug.chrome);
267     },
268 
269     destroyContext: function(context, persistedState)
270     {
271         Firebug.ActivableModule.destroyContext.apply(this, arguments);
272 
273         if (context.netProgress)
274         {
275             // Remember existing breakpoints.
276             var persistedPanelState = getPersistedState(context, panelName);
277             persistedPanelState.breakpoints = context.netProgress.breakpoints;
278         }
279 
280         if (Firebug.NetMonitor.isAlwaysEnabled())
281             unmonitorContext(context);
282     },
283 
284     showContext: function(browser, context)
285     {
286         Firebug.ActivableModule.showContext.apply(this, arguments);
287 
288         if (FBTrace.DBG_NET)
289             FBTrace.sysout("net.showContext; " + (context ? context.getName() : "NULL"));
290     },
291 
292     loadedContext: function(context)
293     {
294         if (FBTrace.DBG_NET)
295             FBTrace.sysout("net.loadedContext; Remove temp context (if not removed yet) " + tabId);
296 
297         var tabId = Firebug.getTabIdForWindow(context.browser.contentWindow);
298         delete contexts[tabId];
299 
300         var netProgress = context.netProgress;
301         if (netProgress)
302         {
303             netProgress.loaded = true;
304 
305             // Set Page title and id into all document objects.
306             for (var i=0; i<netProgress.documents.length; i++)
307             {
308                 var doc = netProgress.documents[i];
309                 doc.id = context.uid;
310                 doc.title = context.getTitle();
311             }
312         }
313     },
314 
315     onEnabled: function(context)
316     {
317         if (FBTrace.DBG_NET)
318             FBTrace.sysout("net.onEnabled; "+context.getName());
319 
320         NetHttpActivityObserver.registerObserver();
321 
322         monitorContext(context);
323     },
324 
325     onDisabled: function(context)
326     {
327         if (FBTrace.DBG_NET)
328             FBTrace.sysout("net.onDisabled; "+context.getName());
329 
330         NetHttpActivityObserver.unregisterObserver();
331 
332         unmonitorContext(context);
333     },
334 
335     onResumeFirebug: function()
336     {
337         if (FBTrace.DBG_NET)
338             FBTrace.sysout("net.onResumeFirebug; ");
339 
340         // Resume only if enabled.
341         if (Firebug.NetMonitor.isAlwaysEnabled())
342             TabWatcher.iterateContexts(monitorContext);
343     },
344 
345     onSuspendFirebug: function()
346     {
347         if (FBTrace.DBG_NET)
348             FBTrace.sysout("net.onSuspendFirebug; ");
349 
350         // Suspend only if enabled.
351         if (Firebug.NetMonitor.isAlwaysEnabled())
352             TabWatcher.iterateContexts(unmonitorContext);
353     },
354 
355     togglePersist: function(context)
356     {
357         var panel = context.getPanel(panelName);
358         panel.persistContent = panel.persistContent ? false : true;
359         Firebug.chrome.setGlobalAttribute("cmd_togglePersistNet", "checked", panel.persistContent);
360     }
361 });
362 
363 // ************************************************************************************************
364 
365 /**
366  * @panel Represents a Firebug panel that displayes info about HTTP activity associated with
367  * the current page. This class is derived from <code>Firebug.ActivablePanel</code> in order
368  * to support activation (enable/disable). This allows to avoid (performance) expensive
369  * features if the functionality is not necessary for the user.
370  */
371 function NetPanel() {}
372 NetPanel.prototype = extend(Firebug.ActivablePanel,
373 {
374     name: panelName,
375     searchable: true,
376     editable: true,
377     breakable: true,
378 
379     initialize: function(context, doc)
380     {
381         this.queue = [];
382 
383         if (FBTrace.DBG_NET)
384             FBTrace.sysout("net.NetPanel.initialize; " + context.getName());
385 
386         // we listen for showUI/hideUI for panel activation
387         Firebug.registerUIListener(this);
388 
389         this.onContextMenu = bind(this.onContextMenu, this);
390 
391         Firebug.ActivablePanel.initialize.apply(this, arguments);
392     },
393 
394     destroy: function(state)
395     {
396         Firebug.ActivablePanel.destroy.apply(this, arguments);
397 
398         Firebug.unregisterUIListener(this);
399     },
400 
401     initializeNode : function()
402     {
403         this.panelNode.addEventListener("contextmenu", this.onContextMenu, false);
404 
405         dispatch([Firebug.A11yModel], "onInitializeNode", [this]);
406     },
407 
408     destroyNode : function()
409     {
410         this.panelNode.removeEventListener("contextmenu", this.onContextMenu, false);
411 
412         dispatch([Firebug.A11yModel], "onDestroyNode", [this]);
413     },
414 
415     loadPersistedContent: function(state)
416     {
417         this.initLayout();
418 
419         var tbody = this.table.firstChild;
420         var lastRow = tbody.lastChild.previousSibling;
421 
422         // Move all net-rows from the persistedState to this panel.
423         var prevTableBody = state.panelNode.getElementsByClassName("netTableBody").item(0);
424         if (!prevTableBody)
425             return;
426 
427         var files = [];
428 
429         while (prevTableBody.firstChild)
430         {
431             var row = prevTableBody.firstChild;
432             if (hasClass(row, "netRow") && hasClass(row, "hasHeaders") && !hasClass(row, "history"))
433             {
434                 row.repObject.history = true;
435                 files.push({
436                     file: row.repObject,
437                     offset: 0 + "%",
438                     width: 0 + "%",
439                     elapsed:  -1
440                 });
441             }
442 
443             if (hasClass(row, "netPageRow"))
444             {
445                 removeClass(row, "opened");
446                 tbody.insertBefore(row, lastRow);
447             }
448             else
449                 prevTableBody.removeChild(row);
450         }
451 
452         if (files.length)
453         {
454             var pageRow = NetPage.pageTag.insertRows({page: state}, lastRow)[0];
455             pageRow.files = files;
456 
457             lastRow = tbody.lastChild.previousSibling;
458         }
459 
460         if (this.table.getElementsByClassName("netPageRow").item(0))
461             NetPage.separatorTag.insertRows({}, lastRow);
462 
463         scrollToBottom(this.panelNode);
464     },
465 
466     savePersistedContent: function(state)
467     {
468         Firebug.ActivablePanel.savePersistedContent.apply(this, arguments);
469 
470         state.pageTitle = this.context.getTitle();
471     },
472 
473     // UI Listener
474     showUI: function(browser, context)
475     {
476     },
477 
478     hideUI: function(browser, context)
479     {
480     },
481 
482     show: function(state)
483     {
484         if (FBTrace.DBG_NET)
485             FBTrace.sysout("net.netPanel.show; " + this.context.getName(), state);
486 
487         var enabled = Firebug.NetMonitor.isAlwaysEnabled();
488         this.showToolbarButtons("fbNetButtons", enabled);
489 
490         if (enabled)
491         {
492             Firebug.NetMonitor.disabledPanelPage.hide(this);
493             Firebug.chrome.setGlobalAttribute("cmd_togglePersistNet", "checked", this.persistContent);
494         }
495         else
496         {
497             Firebug.NetMonitor.disabledPanelPage.show(this);
498             this.table = null;
499         }
500 
501         if (!enabled)
502             return;
503 
504         if (!this.filterCategory)
505             this.setFilter(Firebug.netFilterCategory);
506 
507         this.layout();
508 
509         if (!this.layoutInterval)
510             this.layoutInterval = setInterval(bindFixed(this.updateLayout, this), layoutInterval);
511 
512         if (this.wasScrolledToBottom)
513             scrollToBottom(this.panelNode);
514     },
515 
516     hide: function()
517     {
518         if (FBTrace.DBG_NET)
519             FBTrace.sysout("net.netPanel.hide; " + this.context.getName());
520 
521         this.showToolbarButtons("fbNetButtons", false);
522 
523         Firebug.Debugger.syncCommands(this.context);
524 
525         delete this.infoTipURL;  // clear the state that is tracking the infotip so it is reset after next show()
526         this.wasScrolledToBottom = isScrolledToBottom(this.panelNode);
527 
528         clearInterval(this.layoutInterval);
529         delete this.layoutInterval;
530     },
531 
532     updateOption: function(name, value)
533     {
534         if (name == "netFilterCategory")
535         {
536             Firebug.NetMonitor.syncFilterButtons(Firebug.chrome);
537             for (var i = 0; i < TabWatcher.contexts.length; ++i)
538             {
539                 var context = TabWatcher.contexts[i];
540                 Firebug.NetMonitor.onToggleFilter(context, value);
541             }
542         }
543     },
544 
545     updateSelection: function(object)
546     {
547         if (!object)
548             return;
549 
550         var netProgress = this.context.netProgress;
551         var file = netProgress.getRequestFile(object.request);
552         if (!file)
553         {
554             for (var i=0; i<netProgress.requests.length; i++) {
555                 if (safeGetName(netProgress.requests[i]) == object.href) {
556                    file = netProgress.files[i];
557                    break;
558                 }
559             }
560         }
561 
562         if (file)
563         {
564             scrollIntoCenterView(file.row);
565             if (!hasClass(file.row, "opened"))
566                 NetRequestEntry.toggleHeadersRow(file.row);
567         }
568     },
569 
570     getPopupObject: function(target)
571     {
572         var header = getAncestorByClass(target, "netHeaderRow");
573         if (header)
574             return NetRequestTable;
575 
576         return Firebug.ActivablePanel.getPopupObject.apply(this, arguments);
577     },
578 
579     supportsObject: function(object)
580     {
581         return ((object instanceof SourceLink && object.type == "net") ? 2 : 0);
582     },
583 
584     getOptionsMenuItems: function()
585     {
586         return [
587             this.disableCacheOption()
588         ];
589     },
590 
591     disableCacheOption: function()
592     {
593         var BrowserCache = Firebug.NetMonitor.BrowserCache;
594         var disabled = !BrowserCache.isEnabled();
595         return {label: "net.option.Disable Browser Cache", type: "checkbox", checked: disabled,
596             command: bindFixed(BrowserCache.enable, BrowserCache, disabled) };
597     },
598 
599     getContextMenuItems: function(nada, target)
600     {
601         var items = [];
602 
603         var file = Firebug.getRepObject(target);
604         if (!file || !(file instanceof NetFile))
605             return items;
606 
607         var object = Firebug.getObjectByURL(this.context, file.href);
608         var isPost = Utils.isURLEncodedRequest(file, this.context);
609 
610         items.push(
611             {label: "CopyLocation", command: bindFixed(copyToClipboard, FBL, file.href) }
612         );
613 
614         if (isPost)
615         {
616             items.push(
617                 {label: "CopyLocationParameters", command: bindFixed(this.copyParams, this, file) }
618             );
619         }
620 
621         items.push(
622             {label: "CopyRequestHeaders",
623                 command: bindFixed(this.copyHeaders, this, file.requestHeaders) },
624             {label: "CopyResponseHeaders",
625                 command: bindFixed(this.copyHeaders, this, file.responseHeaders) }
626         );
627 
628         if (textFileCategories.hasOwnProperty(file.category))
629         {
630             items.push(
631                 {label: "CopyResponse", command: bindFixed(this.copyResponse, this, file) }
632             );
633         }
634 
635         items.push(
636             "-",
637             {label: "OpenInTab", command: bindFixed(this.openRequestInTab, this, file) }
638         );
639 
640         if (textFileCategories.hasOwnProperty(file.category))
641         {
642             items.push(
643                 {label: "Open Response In New Tab", command: bindFixed(this.openResponseInTab, this, file) }
644             );
645         }
646 
647         if (!file.loaded)
648         {
649             items.push(
650                 "-",
651                 {label: "StopLoading", command: bindFixed(this.stopLoading, this, file) }
652             );
653         }
654 
655         if (object)
656         {
657             var subItems = Firebug.chrome.getInspectMenuItems(object);
658             if (subItems.length)
659             {
660                 items.push("-");
661                 items.push.apply(items, subItems);
662             }
663         }
664 
665         if (file.isXHR)
666         {
667             var bp = this.context.netProgress.breakpoints.findBreakpoint(file.getFileURL());
668 
669             items.push(
670                 "-",
671                 {label: "net.label.Break On XHR", type: "checkbox", checked: !!bp,
672                     command: bindFixed(this.breakOnRequest, this, file) }
673             );
674 
675             if (bp)
676             {
677                 items.push(
678                     {label: "EditBreakpointCondition",
679                         command: bindFixed(this.editBreakpointCondition, this, file) }
680                 );
681             }
682         }
683 
684         return items;
685     },
686 
687     // Context menu commands
688     copyParams: function(file)
689     {
690         var text = Utils.getPostText(file, this.context, true);
691         var url = reEncodeURL(file, text, true);
692         copyToClipboard(url);
693     },
694 
695     copyHeaders: function(headers)
696     {
697         var lines = [];
698         if (headers)
699         {
700             for (var i = 0; i < headers.length; ++i)
701             {
702                 var header = headers[i];
703                 lines.push(header.name + ": " + header.value);
704             }
705         }
706 
707         var text = lines.join("\r\n");
708         copyToClipboard(text);
709     },
710 
711     copyResponse: function(file)
712     {
713         var allowDoublePost = Firebug.getPref(Firebug.prefDomain, "allowDoublePost");
714         if (!allowDoublePost && !file.cacheEntry)
715         {
716             if (!confirm("The response can be re-requested from the server, OK?"))
717                 return;
718         }
719 
720         // Copy response to the clipboard
721         copyToClipboard(Utils.getResponseText(file, this.context));
722     },
723 
724     openRequestInTab: function(file)
725     {
726         openNewTab(file.href, file.postText);
727     },
728 
729     openResponseInTab: function(file)
730     {
731         try
732         {
733             var response = Utils.getResponseText(file, this.context);
734             var inputStream = getInputStreamFromString(response);
735             var stream = CCIN("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream");
736             stream.setInputStream(inputStream);
737             var encodedResponse = btoa(stream.readBytes(stream.available()));
738             var dataURI = "data:" + file.request.contentType + ";base64," + encodedResponse;
739             gBrowser.selectedTab = gBrowser.addTab(dataURI);
740         }
741         catch (err)
742         {
743             if (FBTrace.DBG_ERRORS)
744                 FBTrace.sysout("net.openResponseInTab EXCEPTION", err);
745         }
746     },
747 
748     breakOnRequest: function(file)
749     {
750         if (!file.isXHR)
751             return;
752 
753         // Create new or remove an existing breakpoint.
754         var breakpoints = this.context.netProgress.breakpoints;
755         var url = file.getFileURL();
756         var bp = breakpoints.findBreakpoint(url);
757         if (bp)
758             breakpoints.removeBreakpoint(url);
759         else
760             breakpoints.addBreakpoint(url);
761 
762         this.enumerateRequests(function(currFile)
763         {
764             if (url != currFile.getFileURL())
765                 return;
766 
767             if (bp)
768                 currFile.row.removeAttribute("breakpoint");
769             else
770                 currFile.row.setAttribute("breakpoint", "true");
771         })
772     },
773 
774     stopLoading: function(file)
775     {
776         const NS_BINDING_ABORTED = 0x804b0002;
777 
778         file.request.cancel(NS_BINDING_ABORTED);
779     },
780 
781     // Support for xhr breakpoint conditions.
782     onContextMenu: function(event)
783     {
784         if (!hasClass(event.target, "sourceLine"))
785             return;
786 
787         var row = getAncestorByClass(event.target, "netRow");
788         if (!row)
789             return;
790 
791         var file = row.repObject;
792         var bp = this.context.netProgress.breakpoints.findBreakpoint(file.getFileURL());
793         if (!bp)
794             return;
795 
796         this.editBreakpointCondition(file);
797         cancelEvent(event);
798     },
799 
800     editBreakpointCondition: function(file)
801     {
802         var bp = this.context.netProgress.breakpoints.findBreakpoint(file.getFileURL());
803         if (!bp)
804             return;
805 
806         var condition = bp ? bp.condition : "";
807 
808         this.selectedSourceBox = this.panelNode;
809         Firebug.Editor.startEditing(file.row, condition);
810     },
811 
812     getEditor: function(target, value)
813     {
814         if (!this.conditionEditor)
815             this.conditionEditor = new Firebug.NetMonitor.ConditionEditor(this.document);
816 
817         return this.conditionEditor;
818     },
819 
820     // Support for activation.
821     disablePanel: function(module)
822     {
823         Firebug.ActivablePanel.disablePanel.apply(this, arguments);
824         this.table = null;
825     },
826 
827     breakOnNext: function(breaking)
828     {
829         this.context.breakOnXHR = breaking;
830     },
831 
832     shouldBreakOnNext: function()
833     {
834         return this.context.breakOnXHR;
835     },
836 
837     getBreakOnNextTooltip: function(enabled)
838     {
839         return (enabled ? $STR("net.Disable Break On XHR") : $STR("net.Break On XHR"));
840     },
841 
842     // Support for info tips.
843     showInfoTip: function(infoTip, target, x, y)
844     {
845         var row = getAncestorByClass(target, "netRow");
846         if (row && row.repObject)
847         {
848             if (getAncestorByClass(target, "netTotalSizeCol"))
849             {
850                 var infoTipURL = "netTotalSize";
851                 if (infoTipURL == this.infoTipURL)
852                     return true;
853 
854                 this.infoTipURL = infoTipURL;
855                 return this.populateTotalSizeInfoTip(infoTip, row);
856             }
857             else if (getAncestorByClass(target, "netSizeCol"))
858             {
859                 var infoTipURL = row.repObject.href + "-netsize";
860                 if (infoTipURL == this.infoTipURL && row.repObject == this.infoTipFile)
861                     return true;
862 
863                 this.infoTipURL = infoTipURL;
864                 this.infoTipFile = row.repObject;
865                 return this.populateSizeInfoTip(infoTip, row.repObject);
866             }
867             else if (getAncestorByClass(target, "netTimeCol"))
868             {
869                 var infoTipURL = row.repObject.href + "-nettime";
870                 if (infoTipURL == this.infoTipURL && row.repObject == this.infoTipFile)
871                     return true;
872 
873                 this.infoTipURL = infoTipURL;
874                 this.infoTipFile = row.repObject;
875                 return this.populateTimeInfoTip(infoTip, row.repObject);
876             }
877             else if (hasClass(row, "category-image") &&
878                 !getAncestorByClass(target, "netRowHeader"))
879             {
880                 var infoTipURL = row.repObject.href + "-image";
881                 if (infoTipURL == this.infoTipURL)
882                     return true;
883 
884                 this.infoTipURL = infoTipURL;
885                 return Firebug.InfoTip.populateImageInfoTip(infoTip, row.repObject.href);
886             }
887         }
888     },
889 
890     populateTimeInfoTip: function(infoTip, file)
891     {
892         Firebug.NetMonitor.TimeInfoTip.render(file, infoTip);
893         return true;
894     },
895 
896     populateSizeInfoTip: function(infoTip, file)
897     {
898         Firebug.NetMonitor.SizeInfoTip.render(file, infoTip);
899         return true;
900     },
901 
902     populateTotalSizeInfoTip: function(infoTip, row)
903     {
904         var totalSizeLabel = row.getElementsByClassName("netTotalSizeLabel").item(0);
905         var file = {size: totalSizeLabel.getAttribute("totalSize")};
906         Firebug.NetMonitor.SizeInfoTip.tag.replace({file: file}, infoTip);
907         return true;
908     },
909 
910     // Support for search within the panel.
911     getSearchOptionsMenuItems: function()
912     {
913         return [
914             Firebug.Search.searchOptionMenu("search.Case Sensitive", "searchCaseSensitive"),
915             //Firebug.Search.searchOptionMenu("search.net.Headers", "netSearchHeaders"),
916             //Firebug.Search.searchOptionMenu("search.net.Parameters", "netSearchParameters"),
917             Firebug.Search.searchOptionMenu("search.net.Response Bodies", "netSearchResponseBody")
918         ];
919     },
920 
921     search: function(text, reverse)
922     {
923         if (!text)
924         {
925             delete this.currentSearch;
926             return false;
927         }
928 
929         var row;
930         if (this.currentSearch && text == this.currentSearch.text)
931         {
932             row = this.currentSearch.findNext(true, false, reverse, Firebug.Search.isCaseSensitive(text));
933         }
934         else
935         {
936             this.currentSearch = new NetPanelSearch(this);
937             row = this.currentSearch.find(text, reverse, Firebug.Search.isCaseSensitive(text));
938         }
939 
940         if (row)
941         {
942             var sel = this.document.defaultView.getSelection();
943             sel.removeAllRanges();
944             sel.addRange(this.currentSearch.range);
945 
946             scrollIntoCenterView(row, this.panelNode);
947             dispatch([Firebug.A11yModel], 'onNetMatchFound', [this, text, row]);
948             return true;
949         }
950         else
951         {
952             dispatch([Firebug.A11yModel], 'onNetMatchFound', [this, text, null]);
953             return false;
954         }
955     },
956 
957     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
958 
959     updateFile: function(file)
960     {
961         if (!file.invalid)
962         {
963             file.invalid = true;
964             this.queue.push(file);
965         }
966     },
967 
968     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
969 
970     updateLayout: function()
971     {
972         if (!this.queue.length)
973             return;
974 
975         var rightNow = now();
976         var length = this.queue.length;
977 
978         if (this.panelNode.offsetHeight)
979             this.wasScrolledToBottom = isScrolledToBottom(this.panelNode);
980 
981         this.layout();
982 
983         if (this.wasScrolledToBottom)
984             scrollToBottom(this.panelNode);
985 
986         if (FBTrace.DBG_NET)
987             FBTrace.sysout("net.updateLayout; Layout done, time elapsed: " +
988                 formatTime(now() - rightNow) + " (" + length + ")");
989     },
990 
991     layout: function()
992     {
993         if (!this.queue.length || !this.context.netProgress ||
994             !Firebug.NetMonitor.isAlwaysEnabled())
995             return;
996 
997         this.initLayout();
998 
999         var rightNow = now();
1000         this.updateRowData(rightNow);
1001         this.updateLogLimit(maxQueueRequests);
1002         this.updateTimeline(rightNow);
1003         this.updateSummaries(rightNow);
1004     },
1005 
1006     initLayout: function()
1007     {
1008         if (!this.table)
1009         {
1010             var limitInfo = {
1011                 totalCount: 0,
1012                 limitPrefsTitle: $STRF("LimitPrefsTitle", [Firebug.prefDomain+".net.logLimit"])
1013             };
1014 
1015             this.table = NetRequestTable.tableTag.append({}, this.panelNode);
1016             this.limitRow = NetLimit.createRow(this.table.firstChild, limitInfo);
1017             this.summaryRow =  NetRequestEntry.summaryTag.insertRows({}, this.table.lastChild.lastChild)[0];
1018 
1019             // Update visibility of columns according to the preferences
1020             var hiddenCols = Firebug.getPref(Firebug.prefDomain, "net.hiddenColumns");
1021             if (hiddenCols)
1022                 this.table.setAttribute("hiddenCols", hiddenCols);
1023         }
1024     },
1025 
1026     updateRowData: function(rightNow)
1027     {
1028         var queue = this.queue;
1029         this.queue = [];
1030 
1031         var phase;
1032         var newFileData = [];
1033 
1034         for (var i = 0; i < queue.length; ++i)
1035         {
1036             var file = queue[i];
1037             if (!file.phase)
1038               continue;
1039 
1040             file.invalid = false;
1041 
1042             phase = this.calculateFileTimes(file, phase, rightNow);
1043 
1044             this.updateFileRow(file, newFileData);
1045             this.invalidatePhase(phase);
1046         }
1047 
1048         if (newFileData.length)
1049         {
1050             var tbody = this.table.firstChild;
1051             var lastRow = tbody.lastChild.previousSibling;
1052             this.insertRows(newFileData, lastRow);
1053         }
1054     },
1055 
1056     insertRows: function(files, lastRow)
1057     {
1058         var row = NetRequestEntry.fileTag.insertRows({files: files}, lastRow)[0];
1059 
1060         for (var i = 0; i < files.length; ++i)
1061         {
1062             var file = files[i].file;
1063             row.repObject = file;
1064             file.row = row;
1065 
1066             // Make sure a breakpoint is displayed.
1067             var breakpoints = this.context.netProgress.breakpoints;
1068             if (breakpoints && breakpoints.findBreakpoint(file.getFileURL()))
1069                 row.setAttribute("breakpoint", "true");
1070 
1071             // Allow customization of request entries in the list. A row is represented
1072             // by <TR> HTML element.
1073             dispatch(NetRequestTable.fbListeners, "onCreateRequestEntry", [this, row]);
1074 
1075             row = row.nextSibling;
1076         }
1077     },
1078 
1079     invalidatePhase: function(phase)
1080     {
1081         if (phase && !phase.invalidPhase)
1082         {
1083             phase.invalidPhase = true;
1084             this.invalidPhases = true;
1085         }
1086     },
1087 
1088     updateFileRow: function(file, newFileData)
1089     {
1090         var row = file.row;
1091         if (!row)
1092         {
1093             newFileData.push({
1094                 file: file,
1095                 offset: this.barOffset + "%",
1096                 width: this.barReceivingWidth + "%",
1097                 elapsed: file.loaded ? this.elapsed : -1
1098             });
1099         }
1100         else
1101         {
1102             var sizeLabel = row.childNodes[4].firstChild;
1103 
1104             var sizeText = NetRequestEntry.getSize(file);
1105 
1106             // Show also total downloaded size for requests in progress.
1107             if (file.totalReceived)
1108                 sizeText += " (" + formatSize(file.totalReceived) + ")";
1109 
1110             sizeLabel.firstChild.nodeValue = sizeText;
1111 
1112             var methodLabel = row.childNodes[2].firstChild;
1113             methodLabel.firstChild.nodeValue = NetRequestEntry.getStatus(file);
1114 
1115             var hrefLabel = row.childNodes[1].firstChild;
1116             hrefLabel.firstChild.nodeValue = NetRequestEntry.getHref(file);
1117 
1118             if (file.mimeType)
1119             {
1120                 // Force update category.
1121                 file.category = null;
1122                 for (var category in fileCategories)
1123                     removeClass(row, "category-" + category);
1124                 setClass(row, "category-" + Utils.getFileCategory(file));
1125             }
1126 
1127             if (file.requestHeaders)
1128                 setClass(row, "hasHeaders");
1129 
1130             if (file.fromCache)
1131                 setClass(row, "fromCache");
1132             else
1133                 removeClass(row, "fromCache");
1134 
1135             if (NetRequestEntry.isError(file))
1136                 setClass(row, "responseError");
1137             else
1138                 removeClass(row, "responseError");
1139 
1140             var timeLabel = row.childNodes[5].childNodes[1].lastChild.firstChild;
1141             timeLabel.innerHTML = NetRequestEntry.getElapsedTime({elapsed: this.elapsed});
1142 
1143             if (file.loaded)
1144                 setClass(row, "loaded");
1145             else
1146                 removeClass(row, "loaded");
1147 
1148             if (hasClass(row, "opened"))
1149             {
1150                 var netInfoBox = row.nextSibling.getElementsByClassName("netInfoBody").item(0);
1151                 NetInfoBody.updateInfo(netInfoBox, file, this.context);
1152             }
1153         }
1154     },
1155 
1156     updateTimeline: function(rightNow)
1157     {
1158         var tbody = this.table.firstChild;
1159 
1160         // XXXjoe Don't update rows whose phase is done and layed out already
1161         var phase;
1162         for (var row = tbody.firstChild; row; row = row.nextSibling)
1163         {
1164             var file = row.repObject;
1165 
1166             // Some rows aren't associated with a file (e.g. header, sumarry).
1167             if (!file)
1168                 continue;
1169 
1170             if (!file.loaded)
1171                 continue;
1172 
1173             phase = this.calculateFileTimes(file, phase, rightNow);
1174 
1175             // Get bar nodes
1176             var blockingBar = row.childNodes[5].childNodes[1].childNodes[1];
1177             var resolvingBar = blockingBar.nextSibling;
1178             var connectingBar = resolvingBar.nextSibling;
1179             var sendingBar = connectingBar.nextSibling;
1180             var waitingBar = sendingBar.nextSibling;
1181             var contentLoadBar = waitingBar.nextSibling;
1182             var windowLoadBar = contentLoadBar.nextSibling;
1183             var receivingBar = windowLoadBar.nextSibling;
1184 
1185             // All bars starts at the beginning
1186             resolvingBar.style.left = connectingBar.style.left = sendingBar.style.left =
1187                 blockingBar.style.left =
1188                 waitingBar.style.left = receivingBar.style.left = this.barOffset + "%";
1189 
1190             // Sets width of all bars (using style). The width is computed according to measured timing.
1191             blockingBar.style.width = this.barBlockingWidth + "%";
1192             resolvingBar.style.width = this.barResolvingWidth + "%";
1193             connectingBar.style.width = this.barConnectingWidth + "%";
1194             sendingBar.style.width = this.barSendingWidth + "%";
1195             waitingBar.style.width = this.barWaitingWidth + "%";
1196             receivingBar.style.width = this.barReceivingWidth + "%";
1197 
1198             if (this.contentLoadBarOffset) {
1199                 contentLoadBar.style.left = this.contentLoadBarOffset + "%";
1200                 contentLoadBar.style.display = "block";
1201                 this.contentLoadBarOffset = null;
1202             }
1203 
1204             if (this.windowLoadBarOffset) {
1205                 windowLoadBar.style.left = this.windowLoadBarOffset + "%";
1206                 windowLoadBar.style.display = "block";
1207                 this.windowLoadBarOffset = null;
1208             }
1209         }
1210     },
1211 
1212     calculateFileTimes: function(file, phase, rightNow)
1213     {
1214         var phases = this.context.netProgress.phases;
1215 
1216         if (phase != file.phase)
1217         {
1218             phase = file.phase;
1219             this.phaseStartTime = phase.startTime;
1220             this.phaseEndTime = phase.endTime ? phase.endTime : rightNow;
1221 
1222             // End of the first phase has to respect even the window "onload" event time, which
1223             // can occur after the last received file. This sets the extent of the timeline so,
1224             // the windowLoadBar is visible.
1225             if (phase.windowLoadTime && this.phaseEndTime < phase.windowLoadTime)
1226                 this.phaseEndTime = phase.windowLoadTime;
1227 
1228             this.phaseElapsed = this.phaseEndTime - phase.startTime;
1229         }
1230 
1231         var elapsed = file.loaded ? file.endTime - file.startTime : 0; /*this.phaseEndTime - file.startTime*/
1232         this.barOffset = Math.floor(((file.startTime-this.phaseStartTime)/this.phaseElapsed) * 100);
1233 
1234         var blockingEnd = (file.sendingTime != file.startTime) ? file.sendingTime : file.waitingForTime;
1235 
1236         this.barResolvingWidth = Math.round(((file.connectingTime - file.startTime) / this.phaseElapsed) * 100);
1237         this.barConnectingWidth = Math.round(((file.connectedTime - file.startTime) / this.phaseElapsed) * 100);
1238         this.barBlockingWidth = Math.round(((blockingEnd - file.startTime) / this.phaseElapsed) * 100);
1239         this.barSendingWidth = Math.round(((file.waitingForTime - file.startTime) / this.phaseElapsed) * 100);
1240         this.barWaitingWidth = Math.round(((file.respondedTime - file.startTime) / this.phaseElapsed) * 100);
1241         this.barReceivingWidth = Math.round((elapsed / this.phaseElapsed) * 100);
1242 
1243         // Total request time doesn't include the time spent in queue.
1244         // xxxHonza: since all phases are now graphically distinguished it's easy to
1245         // see blocking requests. It's make sense to display the real total time now.
1246         this.elapsed = elapsed/* - (file.sendingTime - file.connectedTime)*/;
1247 
1248         // The nspr timer doesn't have 1ms precision, so it can happen that entire
1249         // request is executed in l ms (so the total is zero). Let's display at least
1250         // one bar in such a case so the timeline is visible.
1251         if (this.elapsed <= 0)
1252             this.barReceivingWidth = "1";
1253 
1254         // Compute also offset for the contentLoadBar and windowLoadBar, which are
1255         // displayed for the first phase.
1256         if (phase.contentLoadTime)
1257             this.contentLoadBarOffset = Math.floor(((phase.contentLoadTime-this.phaseStartTime)/this.phaseElapsed) * 100);
1258 
1259         if (phase.windowLoadTime)
1260             this.windowLoadBarOffset = Math.floor(((phase.windowLoadTime-this.phaseStartTime)/this.phaseElapsed) * 100);
1261 
1262         return phase;
1263     },
1264 
1265     updateSummaries: function(rightNow, updateAll)
1266     {
1267         if (!this.invalidPhases && !updateAll)
1268             return;
1269 
1270         this.invalidPhases = false;
1271 
1272         var phases = this.context.netProgress.phases;
1273         if (!phases.length)
1274             return;
1275 
1276         var fileCount = 0, totalSize = 0, cachedSize = 0, totalTime = 0;
1277         for (var i = 0; i < phases.length; ++i)
1278         {
1279             var phase = phases[i];
1280             phase.invalidPhase = false;
1281 
1282             var summary = this.summarizePhase(phase, rightNow);
1283             fileCount += summary.fileCount;
1284             totalSize += summary.totalSize;
1285             cachedSize += summary.cachedSize;
1286             totalTime += summary.totalTime
1287         }
1288 
1289         var row = this.summaryRow;
1290         if (!row)
1291             return;
1292 
1293         var countLabel = row.childNodes[1].firstChild;
1294         countLabel.firstChild.nodeValue = $STRP("plural.Request_Count", [fileCount]);
1295 
1296         var sizeLabel = row.childNodes[4].firstChild;
1297         sizeLabel.setAttribute("totalSize", totalSize);
1298         sizeLabel.firstChild.nodeValue = NetRequestEntry.formatSize(totalSize);
1299 
1300         var cacheSizeLabel = row.lastChild.firstChild.firstChild;
1301         cacheSizeLabel.setAttribute("collapsed", cachedSize == 0);
1302         cacheSizeLabel.childNodes[1].firstChild.nodeValue =
1303             NetRequestEntry.formatSize(cachedSize);
1304 
1305         var timeLabel = row.lastChild.firstChild.lastChild.firstChild;
1306         var timeText = NetRequestEntry.formatTime(totalTime);
1307         var firstPhase = phases[0];
1308         if (firstPhase.windowLoadTime)
1309         {
1310             var loadTime = firstPhase.windowLoadTime - firstPhase.startTime;
1311             // xxxHonza: localization?
1312             timeText += " (onload: " + NetRequestEntry.formatTime(loadTime) + ")";
1313         }
1314 
1315         timeLabel.innerHTML = timeText;
1316     },
1317 
1318     summarizePhase: function(phase, rightNow)
1319     {
1320         var cachedSize = 0, totalSize = 0;
1321 
1322         var category = Firebug.netFilterCategory;
1323         if (category == "all")
1324             category = null;
1325 
1326         var fileCount = 0;
1327         var minTime = 0, maxTime = 0;
1328 
1329         for (var i=0; i<phase.files.length; i++)
1330         {
1331             var file = phase.files[i];
1332 
1333             if (!category || file.category == category)
1334             {
1335                 if (file.loaded)
1336                 {
1337                     ++fileCount;
1338 
1339                     if (file.size > 0)
1340                     {
1341                         totalSize += file.size;
1342                         if (file.fromCache)
1343                             cachedSize += file.size;
1344                     }
1345 
1346                     if (!minTime || file.startTime < minTime)
1347                         minTime = file.startTime;
1348                     if (file.endTime > maxTime)
1349                         maxTime = file.endTime;
1350                 }
1351             }
1352         }
1353 
1354         var totalTime = maxTime - minTime;
1355         return {cachedSize: cachedSize, totalSize: totalSize, totalTime: totalTime,
1356                 fileCount: fileCount}
1357     },
1358 
1359     updateLogLimit: function(limit)
1360     {
1361         var netProgress = this.context.netProgress;
1362 
1363         if (!netProgress)  // XXXjjb Honza, please check, I guess we are getting here with the context not setup
1364         {
1365             if (FBTrace.DBG_NET)
1366                 FBTrace.sysout("net.updateLogLimit; NO NET CONTEXT for: " + this.context.getName());
1367             return;
1368         }
1369 
1370         // Must be positive number;
1371         limit = Math.max(0, limit);
1372 
1373         var filesLength = netProgress.files.length;
1374         if (!filesLength || filesLength <= limit)
1375             return;
1376 
1377         // Remove old requests.
1378         var removeCount = Math.max(0, filesLength - limit);
1379         for (var i=0; i<removeCount; i++)
1380         {
1381             var file = netProgress.files[0];
1382             this.removeLogEntry(file);
1383 
1384             // Remove the file occurrence from the queue.
1385             for (var j=0; j<this.queue.length; j++)
1386             {
1387                 if (this.queue[j] == file) {
1388                     this.queue.splice(j, 1);
1389                     j--;
1390                 }
1391             }
1392         }
1393     },
1394 
1395     removeLogEntry: function(file, noInfo)
1396     {
1397         if (!this.removeFile(file))
1398             return;
1399 
1400         if (!this.table || !this.table.firstChild)
1401             return;
1402 
1403         if (file.row)
1404         {
1405             // The file is loaded and there is a row that has to be removed from the UI.
1406             var tbody = this.table.firstChild;
1407             clearDomplate(file.row);
1408             tbody.removeChild(file.row);
1409         }
1410 
1411         if (noInfo || !this.limitRow)
1412             return;
1413 
1414         this.limitRow.limitInfo.totalCount++;
1415 
1416         NetLimit.updateCounter(this.limitRow);
1417 
1418         //if (netProgress.currentPhase == file.phase)
1419         //  netProgress.currentPhase = null;
1420     },
1421 
1422     removeFile: function(file)
1423     {
1424         var netProgress = this.context.netProgress;
1425         var index = netProgress.files.indexOf(file);
1426         if (index == -1)
1427             return false;
1428 
1429         netProgress.files.splice(index, 1);
1430         netProgress.requests.splice(index, 1);
1431 
1432         // Don't forget to remove the phase whose last file has been removed.
1433         var phase = file.phase;
1434 
1435         // xxxHonza: This needs to be examined yet. Looks like the queue contains
1436         // requests from the previous page. When flushed the requestedFile isn't called
1437         // and the phase is not set.
1438         if (!phase)
1439             return true;
1440 
1441         phase.removeFile(file);
1442         if (!phase.files.length)
1443         {
1444             remove(netProgress.phases, phase);
1445 
1446             if (netProgress.currentPhase == phase)
1447                 netProgress.currentPhase = null;
1448         }
1449 
1450         return true;
1451     },
1452 
1453     insertActivationMessage: function()
1454     {
1455         if (!Firebug.NetMonitor.isAlwaysEnabled())
1456             return;
1457 
1458         // Make sure the basic structure of the table panel is there.
1459         this.initLayout();
1460 
1461         // Get the last request row before summary row.
1462         var tbody = this.table.firstChild;
1463         var lastRow = tbody.lastChild.previousSibling;
1464 
1465         // Insert an activation message (if the last row isn't the message already);
1466         if (hasClass(lastRow, "netActivationRow"))
1467             return;
1468 
1469         var message = NetRequestEntry.activationTag.insertRows({}, lastRow)[0];
1470 
1471         if (FBTrace.DBG_NET)
1472             FBTrace.sysout("net.insertActivationMessage; " + this.context.getName(), message);
1473     },
1474 
1475     enumerateRequests: function(fn)
1476     {
1477         if (!this.table)
1478             return;
1479 
1480         var rows = this.table.getElementsByClassName("netRow");
1481         for (var i=0; i<rows.length; i++)
1482         {
1483             var row = rows[i];
1484             var pageRow = hasClass(row, "netPageRow");
1485 
1486             if (hasClass(row, "collapsed") && !pageRow)
1487                 continue;
1488 
1489             if (hasClass(row, "history"))
1490                 continue;
1491 
1492             // Export also history. These requests can be collpased and so not visible.
1493             if (row.files)
1494             {
1495                 for (var j=0; j<row.files.length; j++)
1496                     fn(row.files[j].file);
1497             }
1498 
1499             var file = Firebug.getRepObject(row);
1500             if (file)
1501                 fn(file);
1502         }
1503     },
1504 
1505     setFilter: function(filterCategory)
1506     {
1507         this.filterCategory = filterCategory;
1508 
1509         var panelNode = this.panelNode;
1510         for (var category in fileCategories)
1511         {
1512             if (filterCategory != "all" && category != filterCategory)
1513                 setClass(panelNode, "hideCategory-"+category);
1514             else
1515                 removeClass(panelNode, "hideCategory-"+category);
1516         }
1517     },
1518 
1519     clear: function()
1520     {
1521         clearNode(this.panelNode);
1522 
1523         this.table = null;
1524         this.summaryRow = null;
1525         this.limitRow = null;
1526 
1527         this.queue = [];
1528         this.invalidPhases = false;
1529 
1530         if (this.context.netProgress)
1531             this.context.netProgress.clear();
1532 
1533         if (FBTrace.DBG_NET)
1534             FBTrace.sysout("net.panel.clear; " + this.context.getName());
1535     },
1536 });
1537 
1538 // ************************************************************************************************
1539 
1540 /**
1541  * @domplate Represents a template that is used to render basic content of the net panel.
1542  */
1543 Firebug.NetMonitor.NetRequestTable = domplate(Firebug.Rep, new Firebug.Listener(),
1544 {
1545     inspectable: false,
1546 
1547     tableTag:
1548 
1549         TABLE({"class": "netTable", cellpadding: 0, cellspacing: 0, hiddenCols: "", "role": "treegrid"},
1550             TBODY({"class": "netTableBody", "role" : "presentation"},
1551                 TR({"class": "netHeaderRow netRow focusRow outerFocusRow", onclick: "$onClickHeader", "role": "row"},
1552                     TD({id: "netBreakpointBar", width: "1%", "class": "netHeaderCell",
1553                         "role": "columnheader"},
1554                         " "
1555                     ),
1556                     TD({id: "netHrefCol", width: "18%", "class": "netHeaderCell alphaValue a11yFocus",
1557                         "role": "columnheader"},
1558                         DIV({"class": "netHeaderCellBox",
1559                         title: $STR("net.header.URL Tooltip")},
1560                         $STR("net.header.URL"))
1561                     ),
1562                     TD({id: "netStatusCol", width: "12%", "class": "netHeaderCell alphaValue a11yFocus",
1563                         "role": "columnheader"},
1564                         DIV({"class": "netHeaderCellBox",
1565                         title: $STR("net.header.Status Tooltip")},
1566                         $STR("net.header.Status"))
1567                     ),
1568                     TD({id: "netDomainCol", width: "12%", "class": "netHeaderCell alphaValue a11yFocus",
1569                         "role": "columnheader"},
1570                         DIV({"class": "netHeaderCellBox",
1571                         title: $STR("net.header.Domain Tooltip")},
1572                         $STR("net.header.Domain"))
1573                     ),
1574                     TD({id: "netSizeCol", width: "4%", "class": "netHeaderCell a11yFocus",
1575                         "role": "columnheader"},
1576                         DIV({"class": "netHeaderCellBox",
1577                         title: $STR("net.header.Size Tooltip")},
1578                         $STR("net.header.Size"))
1579                     ),
1580                     TD({id: "netTimeCol", width: "53%", "class": "netHeaderCell a11yFocus",
1581                         "role": "columnheader"},
1582                         DIV({"class": "netHeaderCellBox",
1583                         title: $STR("net.header.Timeline Tooltip")},
1584                         $STR("net.header.Timeline"))
1585                     )
1586                 )
1587             )
1588         ),
1589 
1590     onClickHeader: function(event)
1591     {
1592         if (FBTrace.DBG_NET)
1593             FBTrace.sysout("net.onClickHeader\n");
1594 
1595         // Also support enter key for sorting
1596         if (!isLeftClick(event) && !(event.type == "keypress" && event.keyCode == 13))
1597             return;
1598 
1599         var table = getAncestorByClass(event.target, "netTable");
1600         var column = getAncestorByClass(event.target, "netHeaderCell");
1601         this.sortColumn(table, column);
1602     },
1603 
1604     sortColumn: function(table, col, direction)
1605     {
1606         if (!col)
1607             return;
1608 
1609         var numerical = !hasClass(col, "alphaValue");
1610 
1611         var colIndex = 0;
1612         for (col = col.previousSibling; col; col = col.previousSibling)
1613             ++colIndex;
1614 
1615         // the first breakpoint bar column is not sortable.
1616         if (colIndex == 0)
1617             return;
1618 
1619         this.sort(table, colIndex, numerical, direction);
1620     },
1621 
1622     sort: function(table, colIndex, numerical, direction)
1623     {
1624         var tbody = table.lastChild;
1625         var headerRow = tbody.firstChild;
1626 
1627         // Remove class from the currently sorted column
1628         var headerSorted = getChildByClass(headerRow, "netHeaderSorted");
1629         removeClass(headerSorted, "netHeaderSorted");
1630         if (headerSorted)
1631             headerSorted.removeAttribute("aria-sort");
1632 
1633         // Mark new column as sorted.
1634         var header = headerRow.childNodes[colIndex];
1635         setClass(header, "netHeaderSorted");
1636         // If the column is already using required sort direction, bubble out.
1637         if ((direction == "desc" && header.sorted == 1) ||
1638             (direction == "asc" && header.sorted == -1))
1639             return;
1640         if (header)
1641             header.setAttribute("aria-sort", header.sorted === -1 ? "descending" : "ascending");
1642         var colID = header.getAttribute("id");
1643 
1644         var values = [];
1645         for (var row = tbody.childNodes[1]; row; row = row.nextSibling)
1646         {
1647             if (!row.repObject)
1648                 continue;
1649 
1650             if (hasClass(row, "history"))
1651                 continue;
1652 
1653             var cell = row.childNodes[colIndex];
1654             var value = numerical ? parseFloat(cell.textContent) : cell.textContent;
1655 
1656             if (colID == "netTimeCol")
1657                 value = row.repObject.startTime;
1658             else if (colID == "netSizeCol")
1659                 value = row.repObject.size;
1660 
1661             if (hasClass(row, "opened"))
1662             {
1663                 var netInfoRow = row.nextSibling;
1664                 values.push({row: row, value: value, info: netInfoRow});
1665                 row = netInfoRow;
1666             }
1667             else
1668             {
1669                 values.push({row: row, value: value});
1670             }
1671         }
1672 
1673         values.sort(function(a, b) { return a.value < b.value ? -1 : 1; });
1674 
1675         if ((header.sorted && header.sorted == 1) || (!header.sorted && direction == "asc"))
1676         {
1677             removeClass(header, "sortedDescending");
1678             setClass(header, "sortedAscending");
1679             header.sorted = -1;
1680 
1681             for (var i = 0; i < values.length; ++i)
1682             {
1683                 tbody.appendChild(values[i].row);
1684                 if (values[i].info)
1685                     tbody.appendChild(values[i].info);
1686             }
1687         }
1688         else
1689         {
1690             removeClass(header, "sortedAscending");
1691             setClass(header, "sortedDescending");
1692 
1693             header.sorted = 1;
1694 
1695             for (var i = values.length-1; i >= 0; --i)
1696             {
1697                 tbody.appendChild(values[i].row);
1698                 if (values[i].info)
1699                     tbody.appendChild(values[i].info);
1700             }
1701         }
1702 
1703         // Make sure the summary row is again at the end.
1704         var summaryRow = tbody.getElementsByClassName("netSummaryRow").item(0);
1705         tbody.appendChild(summaryRow);
1706     },
1707 
1708     supportsObject: function(object)
1709     {
1710         return (object == this);
1711     },
1712 
1713     /**
1714      * Provides menu items for header context menu.
1715      */
1716     getContextMenuItems: function(object, target, context)
1717     {
1718         var popup = $("fbContextMenu");
1719         if (popup.firstChild && popup.firstChild.getAttribute("command") == "cmd_copy")
1720             popup.removeChild(popup.firstChild);
1721 
1722         var items = [];
1723 
1724         // Iterate over all columns and create a menu item for each.
1725         var table = context.getPanel(panelName, true).table;
1726         var hiddenCols = table.getAttribute("hiddenCols");
1727 
1728         var lastVisibleIndex;
1729         var visibleColCount = 0;
1730 
1731         // Iterate all columns except of the first one for breakpoints.
1732         var header = getAncestorByClass(target, "netHeaderRow");
1733         var columns = cloneArray(header.childNodes);
1734         columns.shift();
1735         for (var i=0; i<columns.length; i++)
1736         {
1737             var column = columns[i];
1738             var visible = (hiddenCols.indexOf(column.id) == -1);
1739 
1740             items.push({
1741                 label: column.textContent,
1742                 type: "checkbox",
1743                 checked: visible,
1744                 nol10n: true,
1745                 command: bindFixed(this.onShowColumn, this, context, column.id)
1746             });
1747 
1748             if (visible)
1749             {
1750                 lastVisibleIndex = i;
1751                 visibleColCount++;
1752             }
1753         }
1754 
1755         // If the last column is visible, disable its menu item.
1756         if (visibleColCount == 1)
1757             items[lastVisibleIndex].disabled = true;
1758 
1759         items.push("-");
1760         items.push({
1761             label: $STR("net.header.Reset_Header"),
1762             nol10n: true,
1763             command: bindFixed(this.onResetColumns, this, context)
1764         });
1765 
1766         return items;
1767     },
1768 
1769     onShowColumn: function(context, colId)
1770     {
1771         var table = context.getPanel(panelName, true).table;
1772         var hiddenCols = table.getAttribute("hiddenCols");
1773 
1774         // If the column is already presented in the list of hidden columns,
1775         // remove it, otherwise append.
1776         var index = hiddenCols.indexOf(colId);
1777         if (index >= 0)
1778         {
1779             table.setAttribute("hiddenCols", hiddenCols.substr(0,index-1) +
1780                 hiddenCols.substr(index+colId.length));
1781         }
1782         else
1783         {
1784             table.setAttribute("hiddenCols", hiddenCols + " " + colId);
1785         }
1786 
1787         // Store current state into the preferences.
1788         Firebug.setPref(Firebug.prefDomain, "net.hiddenColumns", table.getAttribute("hiddenCols"));
1789     },
1790 
1791     onResetColumns: function(context)
1792     {
1793         var panel = context.getPanel(panelName, true);
1794         var header = panel.panelNode.getElementsByClassName("netHeaderRow").item(0);
1795 
1796         // Reset widths
1797         var columns = header.childNodes;
1798         for (var i=0; i<columns.length; i++)
1799         {
1800             var col = columns[i];
1801             if (col.style)
1802                 col.style.width = "";
1803         }
1804 
1805         // Reset visibility. Only the Status column is hidden by default.
1806         panel.table.setAttribute("hiddenCols", "colStatus");
1807         Firebug.setPref(Firebug.prefDomain, "net.hiddenColumns", "colStatus");
1808     },
1809 });
1810 
1811 var NetRequestTable = Firebug.NetMonitor.NetRequestTable;
1812 
1813 // ************************************************************************************************
1814 
1815 /**
1816  * @domplate Represents a template that is used to render net panel entries.
1817  */
1818 Firebug.NetMonitor.NetRequestEntry = domplate(Firebug.Rep, new Firebug.Listener(),
1819 {
1820     fileTag:
1821         FOR("file", "$files",
1822             TR({"class": "netRow $file.file|getCategory focusRow outerFocusRow",
1823                 onclick: "$onClick", "role": "row", "aria-expanded": "false",
1824                 $hasHeaders: "$file.file|hasRequestHeaders",
1825                 $history: "$file.file.history",
1826                 $loaded: "$file.file.loaded",
1827                 $responseError: "$file.file|isError",
1828                 $fromCache: "$file.file.fromCache",
1829                 $inFrame: "$file.file|getInFrame"},
1830                 TD({"class": "netCol"},
1831                    DIV({"class": "sourceLine netRowHeader",
1832                    onclick: "$onClickRowHeader"},
1833                         " "
1834                    )
1835                 ),
1836                 TD({"class": "netHrefCol netCol a11yFocus", "role": "rowheader"},
1837                     DIV({"class": "netHrefLabel netLabel",
1838                          style: "margin-left: $file.file|getIndent\\px"},
1839                         "$file.file|getHref"
1840                     ),
1841                     DIV({"class": "netFullHrefLabel netHrefLabel",
1842                          style: "margin-left: $file.file|getIndent\\px"},
1843                         "$file.file.href"
1844                     )
1845                 ),
1846                 TD({"class": "netStatusCol netCol a11yFocus", "role": "gridcell"},
1847                     DIV({"class": "netStatusLabel netLabel"}, "$file.file|getStatus")
1848                 ),
1849                 TD({"class": "netDomainCol netCol a11yFocus", "role": "gridcell" },
1850                     DIV({"class": "netDomainLabel netLabel"}, "$file.file|getDomain")
1851                 ),
1852                 TD({"class": "netSizeCol netCol a11yFocus", "role": "gridcell", "aria-describedby": "fbNetSizeInfoTip"},
1853                     DIV({"class": "netSizeLabel netLabel"}, "$file.file|getSize")
1854                 ),
1855                 TD({"class": "netTimeCol netCol a11yFocus", "role": "gridcell", "aria-describedby": "fbNetTimeInfoTip"  },
1856                     DIV({"class": "netLoadingIcon"}),
1857                     DIV({"class": "netBar"},
1858                         " ",
1859                         DIV({"class": "netBlockingBar", style: "left: $file.offset"}),
1860                         DIV({"class": "netResolvingBar", style: "left: $file.offset"}),
1861                         DIV({"class": "netConnectingBar", style: "left: $file.offset"}),
1862                         DIV({"class": "netSendingBar", style: "left: $file.offset"}),
1863                         DIV({"class": "netWaitingBar", style: "left: $file.offset"}),
1864                         DIV({"class": "netContentLoadBar", style: "left: $file.offset"}),
1865                         DIV({"class": "netWindowLoadBar", style: "left: $file.offset"}),
1866                         DIV({"class": "netReceivingBar", style: "left: $file.offset; width: $file.width"},
1867                             SPAN({"class": "netTimeLabel"}, "$file|getElapsedTime")
1868                         )
1869                     )
1870                 )
1871             )
1872         ),
1873 
1874     headTag:
1875         TR({"class": "netHeadRow"},
1876             TD({"class": "netHeadCol", colspan: 6},
1877                 DIV({"class": "netHeadLabel"}, "$doc.rootFile.href")
1878             )
1879         ),
1880 
1881     netInfoTag:
1882         TR({"class": "netInfoRow outerFocusRow", "role" : "row"},
1883             TD({"class": "sourceLine netRowHeader"}),
1884             TD({"class": "netInfoCol", colspan: 5, "role" : "gridcell"})
1885         ),
1886 
1887     activationTag:
1888         TR({"class": "netRow netActivationRow"},
1889             TD({"class": "netCol netActivationLabel", colspan: 6, "role": "status"},
1890                 $STR("net.ActivationMessage")
1891             )
1892         ),
1893 
1894     summaryTag:
1895         TR({"class": "netRow netSummaryRow focusRow outerFocusRow", "role": "row", "aria-live": "polite"},
1896             TD({"class": "netCol"}, " "),
1897             TD({"class": "netCol netHrefCol a11yFocus", "role" : "rowheader"},
1898                 DIV({"class": "netCountLabel netSummaryLabel"}, "-")
1899             ),
1900             TD({"class": "netCol netStatusCol a11yFocus", "role" : "gridcell"}),
1901             TD({"class": "netCol netDomainCol a11yFocus", "role" : "gridcell"}),
1902             TD({"class": "netTotalSizeCol netCol netSizeCol a11yFocus", "role" : "gridcell"},
1903                 DIV({"class": "netTotalSizeLabel netSummaryLabel"}, "0KB")
1904             ),
1905             TD({"class": "netTotalTimeCol netCol netTimeCol a11yFocus", "role" : "gridcell"},
1906                 DIV({"class": "netSummaryBar", style: "width: 100%"},
1907                     DIV({"class": "netCacheSizeLabel netSummaryLabel", collapsed: "true"},
1908                         "(",
1909                         SPAN("0KB"),
1910                         SPAN(" " + $STR("FromCache")),
1911                         ")"
1912                     ),
1913                     DIV({"class": "netTimeBar"},
1914                         SPAN({"class": "netTotalTimeLabel netSummaryLabel"}, "0ms")
1915                     )
1916                 )
1917             )
1918         ),
1919 
1920     onClickRowHeader: function(event)
1921     {
1922         cancelEvent(event);
1923 
1924         var rowHeader = event.target;
1925         if (!hasClass(rowHeader, "netRowHeader"))
1926             return;
1927 
1928         var row = getAncestorByClass(event.target, "netRow");
1929         if (!row)
1930             return;
1931 
1932         var context = Firebug.getElementPanel(row).context;
1933         var panel = context.getPanel(panelName, true);
1934         if (panel)
1935             panel.breakOnRequest(row.repObject);
1936     },
1937 
1938     onClick: function(event)
1939     {
1940         if (isLeftClick(event))
1941         {
1942             var row = getAncestorByClass(event.target, "netRow");
1943             if (row)
1944             {
1945                 // Click on the rowHeader element inserts a breakpoint.
1946                 if (getAncestorByClass(event.target, "netRowHeader"))
1947                     return;
1948 
1949                 this.toggleHeadersRow(row);
1950                 cancelEvent(event);
1951             }
1952         }
1953     },
1954 
1955     toggleHeadersRow: function(row)
1956     {
1957         if (!hasClass(row, "hasHeaders"))
1958             return;
1959 
1960         var file = row.repObject;
1961 
1962         toggleClass(row, "opened");
1963         if (hasClass(row, "opened"))
1964         {
1965             var netInfoRow = this.netInfoTag.insertRows({}, row)[0];
1966             var netInfoCol = netInfoRow.getElementsByClassName("netInfoCol").item(0);
1967             var netInfoBox = NetInfoBody.tag.replace({file: file}, netInfoCol);
1968 
1969             // Notify listeners so additional tabs can be created.
1970             dispatch(NetInfoBody.fbListeners, "initTabBody", [netInfoBox, file]);
1971 
1972             NetInfoBody.selectTabByName(netInfoBox, "Headers");
1973             var category = Utils.getFileCategory(row.repObject);
1974             if (category)
1975                 setClass(netInfoBox, "category-" + category);
1976             row.setAttribute('aria-expanded', 'true');
1977         }
1978         else
1979         {
1980             var netInfoRow = row.nextSibling;
1981             var netInfoBox = netInfoRow.getElementsByClassName("netInfoBody").item(0);
1982 
1983             dispatch(NetInfoBody.fbListeners, "destroyTabBody", [netInfoBox, file]);
1984 
1985             row.parentNode.removeChild(netInfoRow);
1986             row.setAttribute('aria-expanded', 'false');
1987         }
1988     },
1989 
1990     getCategory: function(file)
1991     {
1992         var category = Utils.getFileCategory(file);
1993         if (category)
1994             return "category-" + category;
1995 
1996         return "category-undefined";
1997     },
1998 
1999     getInFrame: function(file)
2000     {
2001         return !!file.document.parent;
2002     },
2003 
2004     getIndent: function(file)
2005     {
2006         // XXXjoe Turn off indenting for now, it's confusing since we don't
2007         // actually place nested files directly below their parent
2008         //return file.document.level * indentWidth;
2009         return 10;
2010     },
2011 
2012     isError: function(file)
2013     {
2014         if (file.aborted)
2015             return true;
2016 
2017         var errorRange = Math.floor(file.responseStatus/100);
2018         return errorRange == 4 || errorRange == 5;
2019     },
2020 
2021     getHref: function(file)
2022     {
2023         return (file.method ? file.method.toUpperCase() : "?") + " " + getFileName(file.href);
2024     },
2025 
2026     getStatus: function(file)
2027     {
2028         var text = "";
2029 
2030         if (file.responseStatus)
2031             text += file.responseStatus + " ";
2032 
2033         if (file.responseStatusText)
2034             text += file.responseStatusText;
2035 
2036         return text ? text : " ";
2037     },
2038 
2039     getDomain: function(file)
2040     {
2041         return getPrettyDomain(file.href);
2042     },
2043 
2044     getSize: function(file)
2045     {
2046         return this.formatSize(file.size);
2047     },
2048 
2049     getElapsedTime: function(file)
2050     {
2051         if (!file.elapsed || file.elapsed < 0)
2052             return "";
2053 
2054         return this.formatTime(file.elapsed);
2055     },
2056 
2057     hasRequestHeaders: function(file)
2058     {
2059         return !!file.requestHeaders;
2060     },
2061 
2062     formatSize: function(bytes)
2063     {
2064         return formatSize(bytes);
2065     },
2066 
2067     formatTime: function(elapsed)
2068     {
2069         // Use formatTime util from the lib.
2070         return formatTime(elapsed);
2071     }
2072 });
2073 
2074 var NetRequestEntry = Firebug.NetMonitor.NetRequestEntry;
2075 
2076 // ************************************************************************************************
2077 
2078 Firebug.NetMonitor.NetPage = domplate(Firebug.Rep,
2079 {
2080     separatorTag:
2081         TR({"class": "netRow netPageSeparatorRow"},
2082             TD({"class": "netCol netPageSeparatorLabel", colspan: 6, "role": "separator"})
2083         ),
2084 
2085     pageTag:
2086         TR({"class": "netRow netPageRow", onclick: "$onPageClick"},
2087             TD({"class": "netCol netPageCol", colspan: 6, "role": "separator"},
2088                 DIV({"class": "netLabel netPageLabel netPageTitle"}, "$page|getTitle")
2089             )
2090         ),
2091 
2092     getTitle: function(page)
2093     {
2094         return page.pageTitle;
2095     },
2096 
2097     onPageClick: function(event)
2098     {
2099         if (!isLeftClick(event))
2100             return;
2101 
2102         var target = event.target;
2103         var pageRow = getAncestorByClass(event.target, "netPageRow");
2104         var panel = Firebug.getElementPanel(pageRow);
2105 
2106         if (!hasClass(pageRow, "opened"))
2107         {
2108             setClass(pageRow, "opened");
2109 
2110             var files = pageRow.files;
2111 
2112             // Move all net-rows from the persistedState to this panel.
2113             panel.insertRows(files, pageRow);
2114 
2115             for (var i=0; i<files.length; i++)
2116                 panel.queue.push(files[i].file);
2117 
2118             panel.layout();
2119         }
2120         else
2121         {
2122             removeClass(pageRow, "opened");
2123 
2124             var nextRow = pageRow.nextSibling;
2125             while (!hasClass(nextRow, "netPageRow") && !hasClass(nextRow, "netPageSeparatorRow"))
2126             {
2127                 var nextSibling = nextRow.nextSibling;
2128                 nextRow.parentNode.removeChild(nextRow);
2129                 nextRow = nextSibling;
2130             }
2131         }
2132     },
2133 });
2134 
2135 var NetPage = Firebug.NetMonitor.NetPage;
2136 
2137 // ************************************************************************************************
2138 
2139 /**
2140  * @domplate Represents a template that is used to reneder detailed info about a request.
2141  * This template is rendered when a request is expanded.
2142  */
2143 Firebug.NetMonitor.NetInfoBody = domplate(Firebug.Rep, new Firebug.Listener(),
2144 {
2145     tag:
2146         DIV({"class": "netInfoBody", _repObject: "$file"},
2147             TAG("$infoTabs", {file: "$file"}),
2148             TAG("$infoBodies", {file: "$file"})
2149         ),
2150 
2151     infoTabs:
2152         DIV({"class": "netInfoTabs focusRow subFocusRow", "role": "tablist"},
2153             A({"class": "netInfoParamsTab netInfoTab a11yFocus", onclick: "$onClickTab", "role": "tab",
2154                 view: "Params",
2155                 $collapsed: "$file|hideParams"},
2156                 $STR("URLParameters")
2157             ),
2158             A({"class": "netInfoHeadersTab netInfoTab a11yFocus", onclick: "$onClickTab", "role": "tab",
2159                 view: "Headers"},
2160                 $STR("Headers")
2161             ),
2162             A({"class": "netInfoPostTab netInfoTab a11yFocus", onclick: "$onClickTab", "role": "tab",
2163                 view: "Post",
2164                 $collapsed: "$file|hidePost"},
2165                 $STR("Post")
2166             ),
2167             A({"class": "netInfoPutTab netInfoTab a11yFocus", onclick: "$onClickTab", "role": "tab",
2168                 view: "Put",
2169                 $collapsed: "$file|hidePut"},
2170                 $STR("Put")
2171             ),
2172             A({"class": "netInfoResponseTab netInfoTab a11yFocus", onclick: "$onClickTab", "role": "tab",
2173                 view: "Response",
2174                 $collapsed: "$file|hideResponse"},
2175                 $STR("Response")
2176             ),
2177             A({"