1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/xpcom",
  7     "firebug/net/requestObserver",
  8     "firebug/net/responseObserver",
  9     "firebug/lib/locale",
 10     "firebug/lib/events",
 11     "firebug/lib/url",
 12     "firebug/lib/http",
 13     "firebug/lib/string",
 14     "firebug/chrome/window",
 15     "firebug/net/jsonViewer",
 16     "firebug/trace/traceModule",
 17     "firebug/trace/traceListener",
 18     "firebug/js/sourceCache"
 19 ],
 20 function(Obj, Firebug, Xpcom, HttpRequestObserver, HttpResponseObserver, Locale, Events,
 21     Url, Http, Str, Win, JSONViewerModel, TraceModule, TraceListener) {
 22 
 23 // ********************************************************************************************* //
 24 // Constants
 25 
 26 const Cc = Components.classes;
 27 const Ci = Components.interfaces;
 28 
 29 const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
 30 const prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
 31 const versionChecker = Cc["@mozilla.org/xpcom/version-comparator;1"].getService(Ci.nsIVersionComparator);
 32 const appInfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
 33 
 34 // Maximum cached size of a single response (bytes)
 35 var responseSizeLimit = 1024 * 1024 * 5;
 36 
 37 // List of text content types. These content-types are cached.
 38 var contentTypes =
 39 {
 40     "text/plain": 1,
 41     "text/html": 1,
 42     "text/xml": 1,
 43     "text/xsl": 1,
 44     "text/xul": 1,
 45     "text/css": 1,
 46     "text/sgml": 1,
 47     "text/rtf": 1,
 48     "text/x-setext": 1,
 49     "text/richtext": 1,
 50     "text/javascript": 1,
 51     "text/jscript": 1,
 52     "text/tab-separated-values": 1,
 53     "text/rdf": 1,
 54     "text/xif": 1,
 55     "text/ecmascript": 1,
 56     "text/vnd.curl": 1,
 57     "text/x-json": 1,
 58     "text/x-js": 1,
 59     "text/js": 1,
 60     "text/vbscript": 1,
 61     "view-source": 1,
 62     "view-fragment": 1,
 63     "application/xml": 1,
 64     "application/xhtml+xml": 1,
 65     "application/atom+xml": 1,
 66     "application/rss+xml": 1,
 67     "application/mathml+xml": 1,
 68     "application/rdf+xml": 1,
 69     "application/vnd.mozilla.maybe.feed": 1,
 70     "application/vnd.mozilla.xul+xml": 1,
 71     "application/javascript": 1,
 72     "application/x-javascript": 1,
 73     "application/x-httpd-php": 1,
 74     "application/rdf+xml": 1,
 75     "application/ecmascript": 1,
 76     "application/http-index-format": 1,
 77     "application/json": 1,
 78     "application/x-js": 1,
 79     "multipart/mixed" : 1,
 80     "multipart/x-mixed-replace" : 1,
 81     "image/svg+xml" : 1
 82 };
 83 
 84 // ********************************************************************************************* //
 85 // Model implementation
 86 
 87 /**
 88  * Implementation of cache model. The only purpose of this object is to register an HTTP
 89  * observer, so that HTTP communication can be intercepted and all incoming data stored
 90  * within a cache.
 91  */
 92 Firebug.TabCacheModel = Obj.extend(Firebug.ActivableModule,
 93 {
 94     dispatchName: "tabCache",
 95     contentTypes: contentTypes,
 96     fbListeners: [],
 97 
 98     initialize: function()
 99     {
100         Firebug.ActivableModule.initialize.apply(this, arguments);
101 
102         this.traceListener = new TraceListener("tabCache.", "DBG_CACHE", false);
103         TraceModule.addListener(this.traceListener);
104     },
105 
106     initializeUI: function(owner)
107     {
108         Firebug.ActivableModule.initializeUI.apply(this, arguments);
109 
110         if (FBTrace.DBG_CACHE)
111             FBTrace.sysout("tabCache.initializeUI;");
112 
113         // Read maximum size limit for cached response from preferences.
114         responseSizeLimit = Firebug.Options.get("cache.responseLimit");
115 
116         // Read additional text MIME types from preferences.
117         var mimeTypes = Firebug.Options.get("cache.mimeTypes");
118         if (mimeTypes)
119         {
120             var list = mimeTypes.split(" ");
121             for (var i=0; i<list.length; i++)
122                 contentTypes[list[i]] = 1;
123         }
124 
125         // Merge with JSON types
126         var jsonTypes = JSONViewerModel.contentTypes;
127         for (var p in jsonTypes)
128             contentTypes[p] = 1;
129     },
130 
131     onObserverChange: function(observer)
132     {
133         if (FBTrace.DBG_CACHE)
134             FBTrace.sysout("tabCache.onObserverChange; hasObservers: " + this.hasObservers());
135 
136         // If Firebug is in action, we need to test to see if we need to addObserver
137         if (!Firebug.getSuspended())
138             this.onResumeFirebug();
139     },
140 
141     onResumeFirebug: function()
142     {
143         if (FBTrace.DBG_CACHE)
144             FBTrace.sysout("tabCache.onResumeFirebug; hasObsevers: " + this.hasObservers());
145 
146         if (this.hasObservers() && !this.observing)
147         {
148             HttpRequestObserver.addObserver(this, "firebug-http-event", false);
149             this.observing = true;
150         }
151     },
152 
153     onSuspendFirebug: function()
154     {
155         if (FBTrace.DBG_CACHE)
156             FBTrace.sysout("tabCache.onSuspendFirebug; hasObsevers: " + this.hasObservers());
157 
158         if (this.observing)
159         {
160             HttpRequestObserver.removeObserver(this, "firebug-http-event");
161             this.observing = false;
162         }
163     },
164 
165     shutdown: function()
166     {
167         if (FBTrace.DBG_CACHE)
168             FBTrace.sysout("tabCache.shutdown; Cache model destroyed.");
169 
170         TraceModule.removeListener(this.traceListener);
171 
172         if (this.observing)
173             HttpRequestObserver.removeObserver(this, "firebug-http-event");
174     },
175 
176     initContext: function(context)
177     {
178         if (FBTrace.DBG_CACHE)
179             FBTrace.sysout("tabCache.initContext for: " + context.getName());
180     },
181 
182     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
183     // nsIObserver
184 
185     observe: function(subject, topic, data)
186     {
187         try
188         {
189             if (!(subject instanceof Ci.nsIHttpChannel))
190                 return;
191 
192             // XXXjjb this same code is in net.js, better to have it only once
193             var win = Http.getWindowForRequest(subject);
194             if (!win)
195             {
196                 if (FBTrace.DBG_CACHE)
197                     FBTrace.sysout("tabCache.observe; " + topic + ", NO WINDOW");
198                 return;
199             }
200 
201             if (topic == "http-on-modify-request")
202                 this.onModifyRequest(subject, win);
203             else if (topic == "http-on-examine-response")
204                 this.onExamineResponse(subject, win);
205             else if (topic == "http-on-examine-cached-response")
206                 this.onCachedResponse(subject, win);
207         }
208         catch (err)
209         {
210             if (FBTrace.DBG_ERRORS)
211                 FBTrace.sysout("tabCache.observe EXCEPTION", err);
212         }
213     },
214 
215     onModifyRequest: function(request, win)
216     {
217     },
218 
219     onExamineResponse: function(request, win)
220     {
221         this.registerStreamListener(request, win);
222     },
223 
224     onCachedResponse: function(request, win)
225     {
226         this.registerStreamListener(request, win);
227     },
228 
229     registerStreamListener: function(request, win, forceRegister)
230     {
231         if (Firebug.getSuspended() && !forceRegister)
232         {
233             if (FBTrace.DBG_CACHE)
234                 FBTrace.sysout("tabCache.registerStreamListener; DO NOT TRACK, " +
235                     "Firebug suspended for: " + Http.safeGetRequestName(request));
236             return;
237         }
238 
239         if (!this.hasObservers())
240             return;
241 
242         try
243         {
244             if (FBTrace.DBG_CACHE)
245                 FBTrace.sysout("tabCache.registerStreamListener; " +
246                     Http.safeGetRequestName(request));
247 
248             HttpResponseObserver.register(win, request, new ChannelListenerProxy(win));
249         }
250         catch (err)
251         {
252             if (FBTrace.DBG_ERRORS)
253                 FBTrace.sysout("tabCache.Register Traceable Listener EXCEPTION", err);
254         }
255     },
256 
257     shouldCacheRequest: function(request)
258     {
259         if (!(request instanceof Ci.nsIHttpChannel))
260             return;
261 
262         // Allow to customize caching rules.
263         if (Events.dispatch2(this.fbListeners, "shouldCacheRequest", [request]))
264             return true;
265 
266         // Cache only text responses for now.
267         var contentType = request.contentType;
268         if (contentType)
269             contentType = contentType.split(";")[0];
270 
271         contentType = Str.trim(contentType);
272         if (contentTypes[contentType])
273             return true;
274 
275         // Hack to work around application/octet-stream for js files (see issue 2063).
276         // Let's cache all files with js extensions.
277         var extension = Url.getFileExtension(Http.safeGetRequestName(request));
278         if (extension == "js")
279             return true;
280 
281         if (FBTrace.DBG_CACHE)
282             FBTrace.sysout("tabCache.shouldCacheRequest; Request not cached: " +
283                 request.contentType + ", " + Http.safeGetRequestName(request));
284 
285         return false;
286     },
287 });
288 
289 // ********************************************************************************************* //
290 // Tab Cache
291 
292 /**
293  * This cache object is intended to cache all responses made by a specific tab.
294  * The implementation is based on nsITraceableChannel interface introduced in
295  * Firefox 3.0.4. This interface allows to intercept all incoming HTTP data.
296  *
297  * This object replaces the SourceCache, which still exist only for backward
298  * compatibility.
299  *
300  * The object is derived from SourceCache, so the same interface and most of the
301  * implementation is used.
302  */
303 Firebug.TabCache = function(context)
304 {
305     if (FBTrace.DBG_CACHE)
306         FBTrace.sysout("tabCache.TabCache Created for: " + context.getName());
307 
308     Firebug.SourceCache.call(this, context);
309 };
310 
311 Firebug.TabCache.prototype = Obj.extend(Firebug.SourceCache.prototype,
312 {
313     // Responses in progress
314     responses: [],
315 
316     storePartialResponse: function(request, responseText, win, offset)
317     {
318         if (!offset)
319             offset = 0;
320 
321         if (FBTrace.DBG_CACHE)
322             FBTrace.sysout("tabCache.storePartialResponse " + Http.safeGetRequestName(request),
323                 request.contentCharset);
324 
325         var url = Http.safeGetRequestName(request);
326         var response = this.getResponse(request);
327 
328         // Skip any response data that we have received before (f ex when
329         // response packets are repeated due to quirks in how authentication
330         // requests are projected to the channel listener)
331         var newRawSize = offset + responseText.length;
332         var addRawBytes = newRawSize - response.rawSize;
333 
334         if (responseText.length > addRawBytes)
335             responseText = responseText.substr(responseText.length - addRawBytes);
336 
337         try
338         {
339             responseText = Str.convertToUnicode(responseText, win.document.characterSet);
340         }
341         catch (err)
342         {
343             if (FBTrace.DBG_ERRORS || FBTrace.DBG_CACHE)
344                 FBTrace.sysout("tabCache.storePartialResponse EXCEPTION " +
345                     Http.safeGetRequestName(request), err);
346 
347             // Even responses that are not converted are stored into the cache.
348             // return false;
349         }
350 
351         // Size of each response is limited.
352         var limitNotReached = true;
353         if (response.size + responseText.length >= responseSizeLimit)
354         {
355             limitNotReached = false;
356             responseText = responseText.substr(0, responseSizeLimit - response.size);
357             FBTrace.sysout("tabCache.storePartialResponse Max size limit reached for: " + url);
358         }
359 
360         response.size += responseText.length;
361         response.rawSize = newRawSize;
362 
363         // Store partial content into the cache.
364         this.store(url, responseText);
365 
366         // Return false if furhter parts of this response should be ignored.
367         return limitNotReached;
368     },
369 
370     getResponse: function(request)
371     {
372         var url = Http.safeGetRequestName(request);
373         var response = this.responses[url];
374         if (!response)
375         {
376             this.invalidate(url);
377             this.responses[url] = response = {
378                 request: request,
379                 size: 0,
380                 rawSize: 0
381             };
382         }
383 
384         return response;
385     },
386 
387     storeSplitLines: function(url, lines)
388     {
389         if (FBTrace.DBG_CACHE)
390             FBTrace.sysout("tabCache.storeSplitLines: " + url, lines);
391 
392         var currLines = this.cache[url];
393         if (!currLines)
394             currLines = this.cache[url] = [];
395 
396         // Join the last line with the new first one to make the source code
397         // lines properly formatted...
398         if (currLines.length && lines.length)
399         {
400             // ... but only if the last line isn't already completed.
401             var lastLine = currLines[currLines.length-1];
402             if (lastLine && lastLine.search(/\r|\n/) == -1)
403                 currLines[currLines.length-1] += lines.shift();
404         }
405 
406         // Append new lines (if any) into the array for specified url.
407         if (lines.length)
408             this.cache[url] = currLines.concat(lines);
409 
410         return this.cache[url];
411     },
412 
413     loadFromCache: function(url, method, file)
414     {
415         // The ancestor implementation (SourceCache) uses ioService.newChannel, which
416         // can result in additional request to the server (in case the response can't
417         // be loaded from the Firefox cache) - known as the double-load problem.
418         // This new implementation (TabCache) uses nsITraceableChannel, so all responses
419         // should be already cached.
420 
421         // xxxHonza: TODO entire implementation of this method should be removed in Firebug 1.5
422         // xxxHonza: let's try to get the response from the cache till #449198 is fixed.
423         var stream;
424         var responseText;
425         try
426         {
427             if (!url)
428                 return responseText;
429 
430             if (url === "<unknown>")
431                 return [Locale.$STR("message.sourceNotAvailableFor") + ": " + url];
432 
433             var channel = ioService.newChannel(url, null, null);
434 
435             // These flag combination doesn't repost the request.
436             channel.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE |
437                 Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
438                 Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
439 
440             var charset = "UTF-8";
441 
442             if (!this.context.window)
443             {
444                 if (FBTrace.DBG_ERRORS)
445                 {
446                     FBTrace.sysout("tabCache.loadFromCache; ERROR this.context.window " +
447                         "is undefined");
448                 }
449             }
450 
451             var doc = this.context.window ? this.context.window.document : null;
452             if (doc)
453                 charset = doc.characterSet;
454 
455             stream = channel.open();
456 
457             // The response doesn't have to be in the browser cache.
458             if (!stream.available())
459             {
460                 if (FBTrace.DBG_CACHE)
461                     FBTrace.sysout("tabCache.loadFromCache; Failed to load source for: " + url);
462 
463                 stream.close();
464                 return [Locale.$STR("message.sourceNotAvailableFor") + ": " + url];
465             }
466 
467             // Don't load responses that shouldn't be cached.
468             if (!Firebug.TabCacheModel.shouldCacheRequest(channel))
469             {
470                 if (FBTrace.DBG_CACHE)
471                     FBTrace.sysout("tabCache.loadFromCache; The resource from this URL is not text: " + url);
472 
473                 stream.close();
474                 return [Locale.$STR("message.The resource from this URL is not text") + ": " + url];
475             }
476 
477             responseText = Http.readFromStream(stream, charset);
478 
479             if (FBTrace.DBG_CACHE)
480                 FBTrace.sysout("tabCache.loadFromCache (response coming from FF Cache) " +
481                     url, responseText);
482 
483             responseText = this.store(url, responseText);
484         }
485         catch (err)
486         {
487             if (FBTrace.DBG_ERRORS || FBTrace.DBG_CACHE)
488                 FBTrace.sysout("tabCache.loadFromCache EXCEPTION on url \'" + url +"\'", err);
489         }
490         finally
491         {
492             if (stream)
493                 stream.close();
494         }
495 
496         return responseText;
497     },
498 
499     // nsIStreamListener - callbacks from channel stream listener component.
500     onStartRequest: function(request, requestContext)
501     {
502         if (FBTrace.DBG_CACHE)
503             FBTrace.sysout("tabCache.channel.startRequest: " + Http.safeGetRequestName(request));
504 
505         // Make sure the response-entry (used to count total response size) is properly
506         // initialized (cleared) now. If no data is received, the response entry remains empty.
507         var response = this.getResponse(request);
508 
509         Events.dispatch(Firebug.TabCacheModel.fbListeners, "onStartRequest", [this.context, request]);
510         Events.dispatch(this.fbListeners, "onStartRequest", [this.context, request]);
511     },
512 
513     onDataAvailable: function(request, requestContext, inputStream, offset, count)
514     {
515         if (FBTrace.DBG_CACHE)
516             FBTrace.sysout("tabCache.channel.onDataAvailable: " + Http.safeGetRequestName(request));
517 
518         // If the stream is read a new one must be provided (the stream doesn't implement
519         // nsISeekableStream).
520         var stream = {
521             value: inputStream
522         };
523 
524         Events.dispatch(Firebug.TabCacheModel.fbListeners, "onDataAvailable",
525             [this.context, request, requestContext, stream, offset, count]);
526         Events.dispatch(this.fbListeners, "onDataAvailable", [this.context,
527             request, requestContext, stream, offset, count]);
528 
529         return stream.value;
530     },
531 
532     onStopRequest: function(request, requestContext, statusCode)
533     {
534         // The response has been received; remove the request from the list of
535         // current responses.
536         var url = Http.safeGetRequestName(request);
537         delete this.responses[url];
538 
539         var lines = this.cache[this.removeAnchor(url)];
540         var responseText = lines ? lines.join("") : "";
541 
542         if (FBTrace.DBG_CACHE)
543             FBTrace.sysout("tabCache.channel.stopRequest: " + Http.safeGetRequestName(request),
544                 responseText);
545 
546         Events.dispatch(Firebug.TabCacheModel.fbListeners, "onStopRequest",
547             [this.context, request, responseText]);
548         Events.dispatch(this.fbListeners, "onStopRequest", [this.context, request, responseText]);
549     }
550 });
551 
552 // ********************************************************************************************* //
553 // Proxy Listener
554 
555 function ChannelListenerProxy(win)
556 {
557     this.window = win;
558 }
559 
560 ChannelListenerProxy.prototype =
561 {
562     onStartRequest: function(request, requestContext)
563     {
564         var context = this.getContext();
565         if (context)
566             context.sourceCache.onStartRequest(request, requestContext);
567     },
568 
569     onDataAvailable: function(request, requestContext, inputStream, offset, count)
570     {
571         var context = this.getContext();
572         if (!context)
573             return null;
574 
575         return context.sourceCache.onDataAvailable(request, requestContext,
576             inputStream, offset, count);
577     },
578 
579     onStopRequest: function(request, requestContext, statusCode)
580     {
581         var context = this.getContext();
582         if (context)
583             context.sourceCache.onStopRequest(request, requestContext, statusCode);
584     },
585 
586     onCollectData: function(request, data, offset)
587     {
588         var context = this.getContext();
589         if (!context)
590         {
591             if (FBTrace.DBG_CACHE)
592                 FBTrace.sysout("tabCache.channel.onCollectData: NO CONTEXT " +
593                     Http.safeGetRequestName(request), data);
594 
595             return false;
596         }
597 
598         // Store received data into the cache as they come. If the method returns
599         // false, the rest of the response is ignored (not cached). This is used
600         // to limit size of a cached response.
601         return context.sourceCache.storePartialResponse(request, data, this.window, offset);
602     },
603 
604     getContext: function()
605     {
606         try
607         {
608             return Firebug.connection.getContextByWindow(this.window);
609         }
610         catch (e)
611         {
612         }
613         return null;
614     },
615 
616     shouldCacheRequest: function(request)
617     {
618         try
619         {
620             return Firebug.TabCacheModel.shouldCacheRequest(request)
621         }
622         catch (err)
623         {
624         }
625         return false;
626     },
627 }
628 
629 // ********************************************************************************************* //
630 // Registration
631 
632 Firebug.registerActivableModule(Firebug.TabCacheModel);
633 
634 return Firebug.TabCacheModel;
635 
636 // ********************************************************************************************* //
637 });
638