1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/locale",
  7     "firebug/lib/events",
  8     "firebug/lib/url",
  9     "firebug/js/sourceLink",
 10     "firebug/lib/http",
 11     "firebug/lib/css",
 12     "firebug/chrome/window",
 13     "firebug/lib/string",
 14     "firebug/lib/array",
 15     "firebug/lib/system",
 16     "firebug/net/netUtils",
 17     "firebug/lib/xpcom"
 18 ],
 19 function(Obj, Firebug, Locale, Events, Url, SourceLink, Http, Css, Win, Str,
 20     Arr, System, NetUtils, Xpcom) {
 21 
 22 // ********************************************************************************************* //
 23 // Constants
 24 
 25 const Cc = Components.classes;
 26 const Ci = Components.interfaces;
 27 const Cr = Components.results;
 28 
 29 const CacheService = Cc["@mozilla.org/network/cache-service;1"];
 30 
 31 var versionChecker = Xpcom.CCSV("@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator");
 32 var appInfo = Xpcom.CCSV("@mozilla.org/xre/app-info;1", "nsIXULAppInfo");
 33 var fx18 = versionChecker.compare(appInfo.version, "18") >= 0;
 34 
 35 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 36 
 37 const reIgnore = /about:|javascript:|resource:|chrome:|jar:/;
 38 const reResponseStatus = /HTTP\/1\.\d\s(\d+)\s(.*)/;
 39 
 40 var cacheSession = null;
 41 var panelName = "net";
 42 
 43 // ********************************************************************************************* //
 44 // Net Progress
 45 
 46 function NetProgress(context)
 47 {
 48     if (FBTrace.DBG_NET)
 49         FBTrace.sysout("net.NetProgress.constructor; " +
 50             (context ? context.getName() : "NULL Context"));
 51 
 52     this.context = context;
 53 
 54     var panel = null;
 55     var queue = [];
 56 
 57     this.post = function(handler, args)
 58     {
 59         if (panel)
 60         {
 61             var file = handler.apply(this, args);
 62             if (file)
 63             {
 64                 panel.updateFile(file);
 65 
 66                 // If the panel isn't currently visible, make sure the limit is up to date.
 67                 if (!panel.layoutInterval)
 68                     panel.updateLogLimit(Firebug.NetMonitor.maxQueueRequests);
 69 
 70                 return file;
 71             }
 72         }
 73         else
 74         {
 75             // The first page request is made before the initContext (known problem).
 76             queue.push(handler, args);
 77         }
 78     };
 79 
 80     this.flush = function()
 81     {
 82         for (var i=0; i<queue.length; i+=2)
 83             this.post(queue[i], queue[i+1]);
 84 
 85         queue = [];
 86     };
 87 
 88     this.activate = function(activePanel)
 89     {
 90         this.panel = panel = activePanel;
 91         if (panel)
 92             this.flush();
 93     };
 94 
 95     this.update = function(file)
 96     {
 97         if (panel)
 98             panel.updateFile(file);
 99     };
100 
101     this.clear = function()
102     {
103         for (var i=0; this.files && i<this.files.length; i++)
104             this.files[i].clear();
105 
106         this.requests = [];
107         this.files = [];
108         this.phases = [];
109         this.documents = [];
110         this.windows = [];
111         this.currentPhase = null;
112 
113         queue = [];
114     };
115 
116     this.clear();
117 }
118 
119 NetProgress.prototype =
120 {
121     dispatchName: "netProgress",
122     panel: null,
123 
124     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
125 
126     requestNumber: 1,
127 
128     openingFile: function openingFile(request, win)
129     {
130         if (!fx18)
131             return;
132 
133         var file = this.getRequestFile(request, win);
134         if (file)
135         {
136             // Parse URL params so, they are available for conditional breakpoints.
137             file.urlParams = Url.parseURLParams(file.href);
138             this.breakOnXHR(file);
139         }
140     },
141 
142     startFile: function startFile(request, win)
143     {
144         // Called asynchronously since Fx17 so, can't be use for Break on XHR
145         // since JS stack is not available at the moment.
146         // See https://bugzilla.mozilla.org/show_bug.cgi?id=800799
147         if (fx18)
148             return;
149 
150         var file = this.getRequestFile(request, win);
151         if (file)
152         {
153             // Parse URL params so, they are available for conditional breakpoints.
154             file.urlParams = Url.parseURLParams(file.href);
155             this.breakOnXHR(file);
156         }
157     },
158 
159     requestedHeaderFile: function requestedHeaderFile(request, time, win, xhr, extraStringData)
160     {
161         var file = this.getRequestFile(request);
162         if (file)
163         {
164             logTime(file, "requestedHeaderFile", time);
165 
166             file.requestHeadersText = extraStringData;
167 
168             this.requestedFile(request, time, win, xhr);
169 
170             Events.dispatch(Firebug.NetMonitor.fbListeners, "onRequest", [this.context, file]);
171         }
172     },
173 
174     // Can be called from onModifyRequest (to catch request start even in case of BF cache) and also
175     // from requestHeaderFile (activity observer)
176     requestedFile: function requestedFile(request, time, win, xhr)
177     {
178         var file = this.getRequestFile(request, win);
179         if (file)
180         {
181             logTime(file, "requestedFile", time);
182 
183             if (FBTrace.DBG_NET_EVENTS)
184                 FBTrace.sysout("net.events.requestedFile +0 " + getPrintableTime() + ", " +
185                     request.URI.path, file);
186 
187             // For cached image files, we may never hear another peep from any observers
188             // after this point, so we have to assume that the file is cached and loaded
189             // until we get a respondedFile call later
190             file.startTime = file.endTime = time;
191             file.resolvingTime = time;
192             file.connectingTime = time;
193             file.connectedTime = time;
194             file.sendingTime = time;
195             file.waitingForTime = time;
196             file.respondedTime = time;
197             file.isXHR = xhr;
198             file.isBackground = request.loadFlags & Ci.nsIRequest.LOAD_BACKGROUND;
199             file.method = request.requestMethod;
200 
201             if (!Ci.nsIHttpActivityDistributor)
202                 NetUtils.getPostText(file, this.context);
203 
204             this.extendPhase(file);
205 
206             return file;
207         }
208         else
209         {
210             if (FBTrace.DBG_NET)
211                 FBTrace.sysout("net.requestedFile no file for request=");
212         }
213     },
214 
215     breakOnXHR: function breakOnXHR(file)
216     {
217         var halt = false;
218         var conditionIsFalse = false;
219 
220         // If there is an enabled breakpoint with condition:
221         // 1) break if the condition is evaluated to true.
222         var breakpoints = this.context.netProgress.breakpoints;
223         var bp = breakpoints ? breakpoints.findBreakpoint(file.getFileURL()) : null;
224         if (bp && bp.checked)
225         {
226             halt = true;
227             if (bp.condition)
228             {
229                 halt = bp.evaluateCondition(this.context, file);
230                 conditionIsFalse = !halt;
231             }
232         }
233 
234         // 2) If break on XHR flag is set and there is no condition evaluated to false,
235         // break with "break on next" breaking cause (this new breaking cause can override
236         // an existing one that is set when evaluating a breakpoint condition).
237         if (this.context.breakOnXHR && !conditionIsFalse)
238         {
239             this.context.breakingCause = {
240                 title: Locale.$STR("net.Break On XHR"),
241                 message: Str.cropString(file.href, 200),
242                 copyAction: Obj.bindFixed(System.copyToClipboard, System, file.href)
243             };
244 
245             halt = true;
246         }
247 
248         // Ignore if there is no reason to break.
249         if (!halt)
250             return;
251 
252         // Even if the execution was stopped at breakpoint reset the global
253         // breakOnXHR flag.
254         this.context.breakOnXHR = false;
255 
256         Firebug.Breakpoint.breakNow(this.context.getPanel(panelName, true));
257     },
258 
259     respondedHeaderFile: function respondedHeaderFile(request, time, extraStringData)
260     {
261         var file = this.getRequestFile(request);
262         if (file)
263         {
264             logTime(file, "respondedHeaderFile", time);
265 
266             file.responseHeadersText = extraStringData;
267         }
268     },
269 
270     bodySentFile: function bodySentFile(request, time)
271     {
272         var file = this.getRequestFile(request);
273         if (file)
274         {
275             logTime(file, "bodySentFile", time);
276 
277             NetUtils.getPostText(file, this.context);
278         }
279     },
280 
281     responseStartedFile: function responseStartedFile(request, time)
282     {
283         var file = this.getRequestFile(request);
284         if (file)
285         {
286             logTime(file, "responseStartedFile", time);
287 
288             if (!file.responseStarted)
289             {
290                 file.respondedTime = time;
291                 file.responseStarted = true;
292             }
293 
294             file.endTime = time;
295             return file;
296         }
297     },
298 
299     respondedFile: function respondedFile(request, time, info)
300     {
301         Events.dispatch(Firebug.NetMonitor.fbListeners, "onExamineResponse", [this.context, request]);
302 
303         var file = this.getRequestFile(request);
304         if (file)
305         {
306             logTime(file, "respondedFile", time);
307 
308             if (!Ci.nsIHttpActivityDistributor)
309             {
310                 file.respondedTime = time;
311                 file.endTime = time;
312 
313                 if (request.contentLength >= 0)
314                     file.size = request.contentLength;
315             }
316 
317             if (info)
318             {
319                 if (info.responseStatus == 304)
320                     file.fromCache = true;
321                 else if (!file.fromCache)
322                     file.fromCache = false;
323             }
324 
325             // respondedFile can be executed asynchronously and getting headers now
326             // could be too late. They could be already replaced by cached headers.
327             if (info.responseHeaders)
328                 file.responseHeaders = info.responseHeaders;
329 
330             // Get also request headers (and perhaps also responseHeaders, they won't be 
331             // replaced if already available).
332             NetUtils.getHttpHeaders(request, file, this.context);
333 
334             if (info)
335             {
336                 file.responseStatus = info.responseStatus;
337                 file.responseStatusText = info.responseStatusText;
338                 file.postText = info.postText;
339             }
340 
341             file.aborted = false;
342 
343             // Use ACTIVITY_SUBTYPE_RESPONSE_COMPLETE to get the info if possible.
344             if (!Ci.nsIHttpActivityDistributor)
345             {
346                 if (file.fromCache)
347                     getCacheEntry(file, this);
348             }
349 
350             if (FBTrace.DBG_NET_EVENTS)
351                 FBTrace.sysout("net.events.respondedFile +" + (NetUtils.now() - file.startTime) + " " +
352                      getPrintableTime() + ", " + request.URI.path, file);
353 
354             // The ACTIVITY_SUBTYPE_TRANSACTION_CLOSE could come earlier.
355             if (file.loaded)
356                 return;
357 
358             this.endLoad(file);
359 
360             // If there is a network error, log it into the Console panel.
361             if (Firebug.showNetworkErrors && Firebug.NetMonitor.NetRequestEntry.isError(file))
362             {
363                 Firebug.Errors.increaseCount(this.context);
364                 var message = "NetworkError: " + Firebug.NetMonitor.NetRequestEntry.getStatus(file) + " - "+file.href;
365                 Firebug.Console.log(message, this.context, "error", null, true, file.getFileLink(message));
366             }
367 
368             Events.dispatch(Firebug.NetMonitor.fbListeners, "onResponse", [this.context, file]);
369             return file;
370         }
371     },
372 
373     respondedCacheFile: function respondedCacheFile(request, time, info)
374     {
375         Events.dispatch(Firebug.NetMonitor.fbListeners, "onExamineCachedResponse",
376             [this.context, request]);
377 
378         var file = this.getRequestFile(request, null, true);
379         if (file)
380         {
381             logTime(file, "respondedCacheFile", time);
382 
383             if (FBTrace.DBG_NET_EVENTS)
384                 FBTrace.sysout("net.events.respondedCacheFile +" + (NetUtils.now() - file.startTime) + " " +
385                      getPrintableTime() + ", " + request.URI.path, file);
386 
387             // on-examine-cache-response is using different timer, do not track response
388             // times from the cache and use the proper waiting time.
389             if (file.waitingStarted)
390                 time = file.waitingForTime;
391 
392             if (!file.responseStarted)
393             {
394                 file.respondedTime = time;
395                 file.responseStarted = true;
396             }
397 
398             file.endTime = time;
399             file.fromBFCache = true;
400             file.fromCache = true;
401             file.aborted = false;
402 
403             try
404             {
405                 if (request instanceof Ci.nsIApplicationCacheChannel)
406                 {
407                     if (request.loadedFromApplicationCache)
408                         file.fromAppCache = true;
409                 }
410             }
411             catch (e)
412             {
413                 if (FBTrace.DBG_ERRORS)
414                     FBTrace.sysout("net.respondedCacheFile ERROR " + e, e);
415             }
416 
417             if (request.contentLength >= 0)
418                 file.size = request.contentLength;
419 
420             NetUtils.getHttpHeaders(request, file, this.context);
421 
422             if (info)
423             {
424                 file.responseStatus = info.responseStatus;
425                 file.responseStatusText = info.responseStatusText;
426                 file.postText = info.postText;
427             }
428 
429             getCacheEntry(file, this);
430 
431             this.endLoad(file);
432 
433             Events.dispatch(Firebug.NetMonitor.fbListeners, "onCachedResponse",
434                 [this.context, file]);
435 
436             return file;
437         }
438         else
439         {
440             if (FBTrace.DBG_NET)
441                 FBTrace.sysout("net.respondedCacheFile; NO FILE FOR " +
442                     Http.safeGetRequestName(request));
443         }
444     },
445 
446     waitingForFile: function waitingForFile(request, time)
447     {
448         var file = this.getRequestFile(request, null, true);
449         if (file)
450         {
451             logTime(file, "waitingForFile", time);
452 
453             if (!file.waitingStarted)
454             {
455                 file.waitingForTime = time;
456                 file.waitingStarted = true;
457             }
458         }
459 
460         // Don't update the UI now (optimization).
461         return null;
462     },
463 
464     sendingFile: function sendingFile(request, time, size)
465     {
466         var file = this.getRequestFile(request, null, true);
467         if (file)
468         {
469             logTime(file, "sendingFile", time);
470 
471             // Remember when the send started.
472             if (!file.sendStarted)
473             {
474                 file.sendingTime = time;
475                 file.waitingForTime = time; // in case waiting-for would never came.
476                 file.sendStarted = true;
477             }
478 
479             // Catch 2.
480             // It can happen that "connected" event sometimes comes after sending,
481             // which doesn't make much sense (Firefox bug?)
482             if (!file.connected)
483             {
484                 file.connected = true;
485                 file.connectedTime = time;
486             }
487 
488             file.totalSent = size;
489 
490             // Catch 1.
491             // Request is sending so reset following flags. There are cases where
492             // RESPONSE_COMPLETE and TRANSACTION_CLOSE came in the middle of
493             // connetion initialization (resolving, connecting, connected).
494             file.loaded = false;
495             file.responseStarted = false;
496 
497             if (FBTrace.DBG_NET_EVENTS)
498                 FBTrace.sysout("net.events.sendingFile +" + (NetUtils.now() - file.startTime) + " " +
499                      getPrintableTime() + ", " + request.URI.path, file);
500         }
501 
502         // Don't update the UI now (optimization).
503         return null;
504     },
505 
506     connectingFile: function connectingFile(request, time)
507     {
508         var file = this.getRequestFile(request, null, true);
509 
510         logTime(file, "connectingFile", time);
511 
512         // Resolving, connecting and connected can come after the file is loaded
513         // (closedFile received). This happens if the response is coming from the 
514         // cache. Just ignore it.
515         if (file && file.loaded)
516             return null;
517 
518         if (file && !file.connectStarted)
519         {
520             file.connectStarted = true;
521             file.connectingTime = time;
522             file.connectedTime = time; // in case connected-to would never came.
523             file.sendingTime = time;  // in case sending-to would never came.
524             file.waitingForTime = time; // in case waiting-for would never came.
525         }
526 
527         // Don't update the UI now (optimization).
528         return null;
529     },
530 
531     connectedFile: function connectedFile(request, time)
532     {
533         var file = this.getRequestFile(request, null, true);
534 
535         logTime(file, "connectedFile", time);
536 
537         if (file && file.loaded)
538             return null;
539 
540         if (file && !file.connected)
541         {
542             file.connected = true;
543             file.connectedTime = time;
544             file.sendingTime = time;  // in case sending-to would never came.
545             file.waitingForTime = time; // in case waiting-for would never came.
546         }
547 
548         // Don't update the UI now (optimization).
549         return null;
550     },
551 
552     receivingFile: function receivingFile(request, time, size)
553     {
554         var file = this.getRequestFile(request, null, true);
555         if (file)
556         {
557             logTime(file, "receivingFile", time);
558 
559             if (FBTrace.DBG_NET_EVENTS)
560                 FBTrace.sysout("net.events.receivingFile +" + time + " " +
561                     getPrintableTime() + ", " +
562                     Str.formatSize(size) + " (" + size + "B), " +
563                     request.URI.path, file);
564 
565             file.endTime = time;
566             file.totalReceived = size;
567 
568             // Update phase's lastFinishedFile in case of long time downloads.
569             // This forces the timeline to have proper extent.
570             if (file.phase && file.phase.endTime < time)
571                 file.phase.lastFinishedFile = file;
572 
573             // Force update UI.
574             if (file.row && Css.hasClass(file.row, "opened"))
575             {
576                 var netInfoBox = file.row.nextSibling.getElementsByClassName("netInfoBody").item(0);
577                 if (netInfoBox)
578                 {
579                     netInfoBox.responsePresented = false;
580                     netInfoBox.htmlPresented = false;
581                 }
582             }
583         }
584 
585         return file;
586     },
587 
588     responseCompletedFile: function responseCompletedFile(request, time, responseSize)
589     {
590         var file = this.getRequestFile(request, null, true);
591         if (file)
592         {
593             logTime(file, "responseCompletedFile", time);
594 
595             if (FBTrace.DBG_NET_EVENTS)
596                 FBTrace.sysout("net.events.responseCompletedFile +" + time + " " +
597                     getPrintableTime() + ", " + request.URI.path, file);
598 
599             if (responseSize >= 0)
600                 file.size = responseSize;
601 
602             // This was only a helper to show download progress.
603             file.totalReceived = 0;
604 
605             // The request is completed, get cache entry.
606             getCacheEntry(file, this);
607 
608             // Sometimes the HTTP-ON-EXAMINE-RESPONSE doesn't come.
609             if (!file.loaded  && file.responseHeadersText)
610             {
611                 var info = null;
612                 var m = file.responseHeadersText.match(reResponseStatus);
613                 if (m.length == 3)
614                     info = {responseStatus: m[1], responseStatusText: m[2]};
615                 this.respondedFile(request, NetUtils.now(), info);
616             }
617 
618             this.updateIPInfo(request, file);
619         }
620 
621         return file;
622     },
623 
624     closedFile: function closedFile(request, time)
625     {
626         var file = this.getRequestFile(request, null, true);
627         if (file)
628         {
629             logTime(file, "closedFile", time);
630 
631             if (FBTrace.DBG_NET_EVENTS)
632                 FBTrace.sysout("net.events.closedFile +" + time + " " +
633                     getPrintableTime() + ", " + request.URI.path);
634 
635             // If the response never came, stop the loading and set time info.
636             // In this case the request is marked with "Timeout" and the
637             // respondedTime is set to the time when ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
638             // is received (after timeout).
639             // If file.responseHeadersText is null the response didn't come.
640             if (!file.loaded && !file.responseHeadersText)
641             {
642                 if (FBTrace.DBG_NET_EVENTS)
643                     FBTrace.sysout("net.events; TIMEOUT " + Http.safeGetRequestName(request));
644 
645                 this.endLoad(file);
646 
647                 file.aborted = true;
648                 if (!file.responseStatusText)
649                     file.responseStatusText = "Aborted";
650 
651                 if (!file.responseStarted)
652                 {
653                     file.respondedTime = time;
654                     file.responseStarted = true;
655                 }
656 
657                 file.endTime = time;
658             }
659         }
660 
661         return file;
662     },
663 
664     resolvingFile: function resolvingFile(request, time)
665     {
666         var file = this.getRequestFile(request, null, true);
667 
668         if (file)
669             logTime(file, "resolvingFile", time);
670 
671         if (file && file.loaded)
672             return null;
673 
674         if (file && !file.resolveStarted)
675         {
676             file.resolveStarted = true;
677             file.resolvingTime = time;
678             file.connectingTime = time; // in case connecting would never came.
679             file.connectedTime = time; // in case connected-to would never came.
680             file.sendingTime = time;  // in case sending-to would never came.
681             file.waitingForTime = time; // in case waiting-for would never came.
682         }
683 
684         return file;
685     },
686 
687     resolvedFile: function resolvedFile(request, time)
688     {
689         return null;
690     },
691 
692     stopFile: function stopFile(request, time, postText, responseText)
693     {
694         var file = this.getRequestFile(request, null, true);
695         if (file)
696         {
697 
698             logTime(file, "stopFile", time);
699 
700             if (FBTrace.DBG_NET_EVENTS)
701                 FBTrace.sysout("net.stopFile +" + (NetUtils.now() - file.startTime) + " " +
702                     getPrintableTime() + ", " + request.URI.path, file);
703 
704             // xxxHonza: spy should measure time using the activity observer too.
705             // Don't ruin the endTime if it was already set.
706             if (file.endTime == file.startTime)
707                 file.endTime = time;
708 
709             file.postText = postText;
710             file.responseText = responseText;
711 
712             NetUtils.getHttpHeaders(request, file, this.context);
713 
714             this.endLoad(file);
715 
716             getCacheEntry(file, this);
717         }
718 
719         return file;
720     },
721 
722     abortFile: function abortFile(request, time, postText, responseText)
723     {
724         var file = this.getRequestFile(request, null, true);
725         if (file)
726         {
727             logTime(file, "abortFile", time);
728 
729             file.aborted = true;
730             file.responseStatusText = "Aborted";
731         }
732 
733         return this.stopFile(request, time, postText, responseText);
734     },
735 
736     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
737     // IP Address and port number
738 
739     updateIPInfo: function(request, file)
740     {
741         file.localAddress = Http.safeGetLocalAddress(request);
742         file.localPort = Http.safeGetLocalPort(request);
743         file.remoteAddress = Http.safeGetRemoteAddress(request);
744         file.remotePort = Http.safeGetRemotePort(request);
745     },
746 
747     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
748 
749     windowPaint: function windowPaint(window, time)
750     {
751         if (FBTrace.DBG_NET)
752             FBTrace.sysout("net.windowPaint +? " + getPrintableTime() + ", " +
753                 window.location.href, this.phases);
754 
755         if (!this.phases.length)
756             return;
757 
758         var phase = this.context.netProgress.currentPhase;
759         var timeStamp = phase.addTimeStamp("MozAfterPaint", "netPaintBar");
760         timeStamp.time = time;
761 
762         // Return the first file, so the layout is updated. I can happen that the
763         // onLoad event is the last one and the graph end-time must be recalculated.
764         return phase.files[0];
765     },
766 
767     timeStamp: function timeStamp(window, time, label)
768     {
769         if (FBTrace.DBG_NET)
770             FBTrace.sysout("net.timeStamp +? " + getPrintableTime() + ", " +
771                 window.location.href, this.phases);
772 
773         if (!this.phases.length)
774             return;
775 
776         var phase = this.context.netProgress.currentPhase;
777         var timeStamp = phase.addTimeStamp(label, "netTimeStampBar");
778         timeStamp.time = time;
779 
780         return phase.files[0];
781     },
782 
783     windowLoad: function windowLoad(window, time)
784     {
785         if (FBTrace.DBG_NET_EVENTS)
786             FBTrace.sysout("net.windowLoad +? " + getPrintableTime() + ", " +
787                 window.location.href, this.phases);
788 
789         if (!this.phases.length)
790             return;
791 
792         // Update all requests that belong to the first phase.
793         var firstPhase = this.phases[0];
794 
795         // Keep the information also in the phase for now, NetExport and other could need it.
796         firstPhase.windowLoadTime = time;
797 
798         var timeStamp = firstPhase.addTimeStamp("load", "netWindowLoadBar");
799         timeStamp.time = time;
800 
801         // Return the first file, so the layout is updated. I can happen that the
802         // onLoad event is the last one and the graph end-time must be recalculated.
803         return firstPhase.files[0];
804     },
805 
806     contentLoad: function contentLoad(window, time)
807     {
808         if (FBTrace.DBG_NET_EVENTS)
809             FBTrace.sysout("net.contentLoad +? " + getPrintableTime() + ", " +
810                 window.location.href);
811 
812         if (!this.phases.length)
813             return;
814 
815         // Update all requests that belong to the first phase.
816         var firstPhase = this.phases[0];
817 
818         // Keep the information also in the phase for now, NetExport and other could need it.
819         firstPhase.contentLoadTime = time;
820 
821         var timeStamp = firstPhase.addTimeStamp("DOMContentLoaded", "netContentLoadBar");
822         timeStamp.time = time;
823 
824         return null;
825     },
826 
827     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
828 
829     getRequestFile: function getRequestFile(request, win, noCreate)
830     {
831         var name = Http.safeGetRequestName(request);
832         if (!name || reIgnore.exec(name))
833             return null;
834 
835         for (var i=0; i<this.files.length; i++)
836         {
837             var file = this.files[i];
838             if (file.request == request)
839                 return file;
840         }
841 
842         if (noCreate)
843             return null;
844 
845         if (!win || Win.getRootWindow(win) != this.context.window)
846             return;
847 
848         var fileDoc = this.getRequestDocument(win);
849         var isDocument = request.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI && fileDoc.parent;
850         var doc = isDocument ? fileDoc.parent : fileDoc;
851 
852         var file = doc.createFile(request);
853         if (isDocument)
854         {
855             fileDoc.documentFile = file;
856             file.ownDocument = fileDoc;
857         }
858 
859         file.request = request;
860         file.requestNumber = this.requestNumber;
861         this.requestNumber++;
862         this.requests.push(request);
863         this.files.push(file);
864 
865         if (FBTrace.DBG_NET_EVENTS)
866             FBTrace.sysout("net.createFile; " + Http.safeGetRequestName(request) +
867                 "(" + this.files.length + ")");
868 
869         return file;
870     },
871 
872     getRequestDocument: function(win)
873     {
874         if (win)
875         {
876             var index = this.windows.indexOf(win);
877             if (index == -1)
878             {
879                 var doc = new NetDocument();
880                 if (win.parent != win)
881                     doc.parent = this.getRequestDocument(win.parent);
882 
883                 //doc.level = NetUtils.getFrameLevel(win);
884 
885                 this.documents.push(doc);
886                 this.windows.push(win);
887 
888                 return doc;
889             }
890             else
891                 return this.documents[index];
892         }
893         else
894             return this.documents[0];
895     },
896 
897     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
898 
899     endLoad: function(file)
900     {
901         if (FBTrace.DBG_NET_EVENTS)
902             FBTrace.sysout("net.events.endLoad +" + (NetUtils.now() - file.startTime) + " " +
903                 getPrintableTime() + ", " + file.request.URI.path, file);
904 
905         // Set file as loaded.
906         file.loaded = true;
907 
908         // Update last finished file of the associated phase.
909         //xxxHonza: verify this.
910         if (file.phase)
911             file.phase.lastFinishedFile = file;
912     },
913 
914     extendPhase: function(file)
915     {
916         // Phase start can be measured since HTTP-ON-MODIFIED-REQUEST as
917         // ACTIVITY_SUBTYPE_REQUEST_HEADER won't fire if the response comes from the BF cache.
918         // If it's real HTTP request we need to start again since ACTIVITY_SUBTYPE_REQUEST_HEADER
919         // has the proper time.
920         // Order of ACTIVITY_SUBTYPE_REQUEST_HEADER can be different than order of
921         // HTTP-ON-MODIFIED-REQUEST events, see issue 4535
922         if (file.phase)
923         {
924             if (file.phase.files[0] == file)
925                 file.phase.startTime = file.startTime;
926 
927             // Since the request order can be wrong (see above) we need to iterate all files
928             // in this phase and find the one that actually executed first.
929             // In some cases, the waterfall can display a request executed before another,
930             // but started later.
931             // See: https://bugzilla.mozilla.org/show_bug.cgi?id=664781
932             var phase = file.phase;
933             for (var i=0; i<phase.files.length; i++)
934             {
935                 var file = phase.files[i];
936                 if (file.startTime > 0 && phase.startTime > file.startTime)
937                     phase.startTime = file.startTime;
938             }
939             return;
940         }
941 
942         if (this.currentPhase)
943         {
944             // If the new request has been started within a "phaseInterval" after the
945             // previous reqeust has been started, associate it with the current phase;
946             // otherwise create a new phase.
947             var phaseInterval = Firebug.netPhaseInterval;
948             var lastStartTime = this.currentPhase.lastStartTime;
949             if (phaseInterval > 0 && this.loaded && file.startTime - lastStartTime >= phaseInterval)
950                 this.startPhase(file);
951             else
952                 this.currentPhase.addFile(file);
953         }
954         else
955         {
956             // If there is no phase yet, just create it.
957             this.startPhase(file);
958         }
959     },
960 
961     startPhase: function(file)
962     {
963         var phase = new NetPhase(file);
964         phase.initial = !this.currentPhase;
965 
966         file.breakLayout = true;
967 
968         this.currentPhase = phase;
969         this.phases.push(phase);
970     },
971 };
972 
973 // ********************************************************************************************* //
974 // Time Logging
975 
976 function logTime(file, title, time)
977 {
978     // xxxHonza: just for debugging purposes.
979     return;
980 
981     if (!file._timings)
982         file._timings = {counter: 0};
983 
984     if (!file._timings.logs)
985         file._timings.logs = [];
986 
987     file._timings.logs.push({
988         title: title,
989         index: ++file._timings.counter,
990         time: time
991     });
992 }
993 
994 // ********************************************************************************************* //
995 
996 /**
997  * A Document is a helper object that represents a document (window) on the page.
998  * This object is created for main page document and for every embedded document (iframe)
999  * for which a request is made.
1000  */
1001 function NetDocument()
1002 {
1003     this.id = 0;
1004     this.title = "";
1005 }
1006 
1007 NetDocument.prototype =
1008 {
1009     createFile: function(request)
1010     {
1011         return new NetFile(request.name, this);
1012     }
1013 };
1014 
1015 // ********************************************************************************************* //
1016 
1017 /**
1018  * A File is a helper object that represents a file for which a request is made.
1019  * The document refers to it's parent document (NetDocument) through a member
1020  * variable.
1021  */
1022 function NetFile(href, document)
1023 {
1024     this.href = href;
1025     this.document = document;
1026 }
1027 
1028 NetFile.prototype =
1029 {
1030     status: 0,
1031     files: 0,
1032     loaded: false,
1033     fromCache: false,
1034     size: -1,
1035     expectedSize: -1,
1036     endTime: null,
1037     waitingForTime: null,
1038     connectingTime: null,
1039 
1040     getFileLink: function(message)
1041     {
1042         // this.SourceLink = function(url, line, type, object, instance)
1043         var link = new SourceLink.SourceLink(this.href, null, "net", this.request);
1044         return link;
1045     },
1046 
1047     getFileURL: function()
1048     {
1049         var index = this.href.indexOf("?");
1050         if (index < 0)
1051             return this.href;
1052 
1053         return this.href.substring(0, index);
1054     },
1055 
1056     clear: function()
1057     {
1058         // Remove all members to avoid circular references and memleaks.
1059         for (var name in this)
1060             delete this[name];
1061     }
1062 };
1063 
1064 Firebug.NetFile = NetFile;
1065 
1066 // ********************************************************************************************* //
1067 
1068 /**
1069  * A Phase is a helper object that groups requests made in the same time frame.
1070  * In other words, if a new requests is started within a given time (specified
1071  * by phaseInterval [ms]) - after previous request has been started -
1072  * it automatically belongs to the same phase.
1073  * If a request is started after this period, a new phase is created
1074  * and this file becomes to be the first in that phase.
1075  * The first phase is ended when the page finishes it's loading. Other phases
1076  * might be started by additional XHR made by the page.
1077  *
1078  * All phases are stored within NetProgress.phases array.
1079  *
1080  * Phases are used to compute size of the graphical timeline. The timeline
1081  * for each phase starts from the beginning of the graph.
1082  */
1083 function NetPhase(file)
1084 {
1085   // Start time of the phase. Remains the same, even if the file
1086   // is removed from the log (due to a max limit of entries).
1087   // This ensures stability of the time line.
1088   this.startTime = file.startTime;
1089 
1090   // The last finished request (file) in the phase.
1091   this.lastFinishedFile = null;
1092 
1093   // Set to true if the phase needs to be updated in the UI.
1094   this.invalidPhase = null;
1095 
1096   // List of files associated with this phase.
1097   this.files = [];
1098 
1099   // List of paint events.
1100   this.windowPaints = [];
1101 
1102   this.timeStamps = [];
1103 
1104   this.addFile(file);
1105 }
1106 
1107 NetPhase.prototype =
1108 {
1109     addFile: function(file)
1110     {
1111         this.files.push(file);
1112         file.phase = this;
1113     },
1114 
1115     removeFile: function removeFile(file)
1116     {
1117         Arr.remove(this.files, file);
1118 
1119         // The file don't have a parent phase now.
1120         file.phase = null;
1121 
1122         // If the last file has been removed, update the last file member.
1123         if (file == this.lastFinishedFile)
1124         {
1125             if (this.files.length == 0)
1126             {
1127                 this.lastFinishedFile = null;
1128             }
1129             else
1130             {
1131                 for (var i=0; i<this.files.length; i++)
1132                 {
1133                     if (this.lastFinishedFile.endTime < this.files[i].endTime)
1134                         this.lastFinishedFile = this.files[i];
1135                 }
1136             }
1137         }
1138     },
1139 
1140     get lastStartTime()
1141     {
1142         return this.files[this.files.length - 1].startTime;
1143     },
1144 
1145     get endTime()
1146     {
1147         var endTime = this.lastFinishedFile ? this.lastFinishedFile.endTime : null;
1148         if (this.timeStamps.length > 0)
1149         {
1150             var lastTimeStamp = this.timeStamps[this.timeStamps.length-1].time;
1151             endTime = (endTime > lastTimeStamp) ? endTime : lastTimeStamp;
1152         }
1153         return endTime;
1154     },
1155 
1156     addTimeStamp: function(label, classes)
1157     {
1158         var timeStamp = {
1159             label: label,
1160             classes: classes
1161         };
1162 
1163         this.timeStamps.push(timeStamp);
1164         return timeStamp;
1165     }
1166 };
1167 
1168 // ********************************************************************************************* //
1169 
1170 function getCacheEntry(file, netProgress)
1171 {
1172     // Bail out if the cache is disabled.
1173     if (!Firebug.NetMonitor.BrowserCache.isEnabled())
1174         return;
1175 
1176     // Don't request the cache entry twice.
1177     if (file.cacheEntryRequested)
1178         return;
1179 
1180     file.cacheEntryRequested = true;
1181 
1182     if (FBTrace.DBG_NET_EVENTS)
1183         FBTrace.sysout("net.getCacheEntry for file.href: " + file.href + "\n");
1184 
1185     // Pause first because this is usually called from stopFile, at which point
1186     // the file's cache entry is locked
1187     setTimeout(function()
1188     {
1189         try
1190         {
1191             delayGetCacheEntry(file, netProgress);
1192         }
1193         catch (exc)
1194         {
1195             if (exc.name != "NS_ERROR_CACHE_KEY_NOT_FOUND")
1196             {
1197                 if (FBTrace.DBG_ERRORS)
1198                     FBTrace.sysout("net.delayGetCacheEntry FAILS " + file.href, exc);
1199             }
1200         }
1201     });
1202 }
1203 
1204 function delayGetCacheEntry(file, netProgress)
1205 {
1206     if (FBTrace.DBG_NET_EVENTS)
1207         FBTrace.sysout("net.delayGetCacheEntry for file.href=" + file.href + "\n");
1208 
1209     // Init cache session.
1210     if (!cacheSession)
1211     {
1212         var cacheService = CacheService.getService(Ci.nsICacheService);
1213         cacheSession = cacheService.createSession("HTTP", Ci.nsICache.STORE_ANYWHERE, true);
1214         cacheSession.doomEntriesIfExpired = false;
1215     }
1216 
1217     cacheSession.asyncOpenCacheEntry(file.href, Ci.nsICache.ACCESS_READ,
1218     {
1219         onCacheEntryAvailable: function(descriptor, accessGranted, status)
1220         {
1221             if (FBTrace.DBG_NET_EVENTS)
1222                 FBTrace.sysout("net.onCacheEntryAvailable for file.href=" + file.href + "\n");
1223 
1224             if (descriptor)
1225             {
1226                 if (file.size <= 0)
1227                     file.size = descriptor.dataSize;
1228 
1229                 if (descriptor.lastModified && descriptor.lastFetched &&
1230                     descriptor.lastModified < Math.floor(file.startTime/1000)) {
1231                     file.fromCache = true;
1232                 }
1233 
1234                 file.cacheEntry = [
1235                   { name: "Last Modified",
1236                     value: NetUtils.getDateFromSeconds(descriptor.lastModified)
1237                   },
1238                   { name: "Last Fetched",
1239                     value: NetUtils.getDateFromSeconds(descriptor.lastFetched)
1240                   },
1241                   { name: "Expires",
1242                     value: NetUtils.getDateFromSeconds(descriptor.expirationTime)
1243                   },
1244                   { name: "Data Size",
1245                     value: descriptor.dataSize
1246                   },
1247                   { name: "Fetch Count",
1248                     value: descriptor.fetchCount
1249                   },
1250                   { name: "Device",
1251                     value: descriptor.deviceID
1252                   }
1253                 ];
1254 
1255                 // Get contentType from the cache.
1256                 try
1257                 {
1258                     var value = descriptor.getMetaDataElement("response-head");
1259                     var contentType = getContentTypeFromResponseHead(value);
1260                     file.mimeType = NetUtils.getMimeType(contentType, file.href);
1261                 }
1262                 catch (e)
1263                 {
1264                     if (FBTrace.DBG_ERRORS)
1265                         FBTrace.sysout("net.delayGetCacheEntry; EXCEPTION ", e);
1266                 }
1267 
1268                 descriptor.close();
1269                 netProgress.update(file);
1270             }
1271 
1272             getCachedHeaders(file);
1273         }
1274     });
1275 }
1276 
1277 function getCachedHeaders(file)
1278 {
1279     // Cached headers are important only if the reqeust comes from the cache.
1280     if (!file.fromCache)
1281         return;
1282 
1283     // The request is containing cached headers now. These will be also displayed
1284     // within the Net panel.
1285     var cache = {};
1286     NetUtils.getHttpHeaders(file.request, cache);
1287     file.cachedResponseHeaders = cache.responseHeaders;
1288 }
1289 
1290 function getContentTypeFromResponseHead(value)
1291 {
1292     var values = value.split("\r\n");
1293     for (var i=0; i<values.length; i++)
1294     {
1295         var option = values[i].split(": ");
1296         var headerName = option[0];
1297         if (headerName && headerName.toLowerCase() == "content-type")
1298             return option[1];
1299     }
1300 }
1301 
1302 // ********************************************************************************************* //
1303 // Helper for tracing
1304 
1305 function getPrintableTime()
1306 {
1307     var date = new Date();
1308     return "(" + date.getSeconds() + ":" + date.getMilliseconds() + ")";
1309 }
1310 
1311 // ********************************************************************************************* //
1312 // Registration
1313 
1314 return NetProgress;
1315 
1316 // ********************************************************************************************* //
1317 });
1318