1 /* See license.txt for terms of usage */
  2 
  3 FBL.ns(function() { with (FBL) {
  4 
  5 // ************************************************************************************************
  6 // Constants
  7 
  8 const Cc = Components.classes;
  9 const Ci = Components.interfaces;
 10 
 11 const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
 12 const prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch2);
 13 const versionChecker = Cc["@mozilla.org/xpcom/version-comparator;1"].getService(Ci.nsIVersionComparator);
 14 const appInfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
 15 
 16 // Maximum cached size of a signle response (bytes)
 17 var responseSizeLimit = 1024 * 1024 * 5;
 18 
 19 // List of text content types. These content-types are cached.
 20 var contentTypes =
 21 {
 22     "text/plain": 1,
 23     "text/html": 1,
 24     "text/xml": 1,
 25     "text/xsl": 1,
 26     "text/xul": 1,
 27     "text/css": 1,
 28     "text/sgml": 1,
 29     "text/rtf": 1,
 30     "text/x-setext": 1,
 31     "text/richtext": 1,
 32     "text/javascript": 1,
 33     "text/jscript": 1,
 34     "text/tab-separated-values": 1,
 35     "text/rdf": 1,
 36     "text/xif": 1,
 37     "text/ecmascript": 1,
 38     "text/vnd.curl": 1,
 39     "text/x-json": 1,
 40     "text/x-js": 1,
 41     "text/js": 1,
 42     "text/vbscript": 1,
 43     "view-source": 1,
 44     "view-fragment": 1,
 45     "application/xml": 1,
 46     "application/xhtml+xml": 1,
 47     "application/vnd.mozilla.xul+xml": 1,
 48     "application/javascript": 1,
 49     "application/x-javascript": 1,
 50     "application/x-httpd-php": 1,
 51     "application/rdf+xml": 1,
 52     "application/ecmascript": 1,
 53     "application/http-index-format": 1,
 54     "application/json": 1,
 55     "application/x-js": 1,
 56     "multipart/mixed" : 1,
 57     "multipart/x-mixed-replace" : 1,
 58     "image/svg+xml" : 1
 59 };
 60 
 61 // ************************************************************************************************
 62 // Model implementation
 63 
 64 /**
 65  * Implementation of cache model. The only purpose of this object is to register an HTTP
 66  * observer so, HTTP communication can be interecepted and all incoming data stored within
 67  * a cache.
 68  */
 69 Firebug.TabCacheModel = extend(Firebug.Module,
 70 {
 71     dispatchName: "tabCache",
 72     contentTypes: contentTypes,
 73     fbListeners: [],
 74 
 75     initializeUI: function(owner)
 76     {
 77         if (FBTrace.DBG_CACHE)
 78             FBTrace.sysout("tabCache.initializeUI; Cache model initialized.");
 79 
 80         // Read maximum size limit for cached response from preferences.
 81         responseSizeLimit = Firebug.getPref(Firebug.prefDomain, "cache.responseLimit");
 82 
 83         // Read additional text mime-types from preferences.
 84         var mimeTypes = Firebug.getPref(Firebug.prefDomain, "cache.mimeTypes");
 85         if (mimeTypes)
 86         {
 87             var list = mimeTypes.split(" ");
 88             for (var i=0; i<list.length; i++)
 89                 contentTypes[list[i]] = 1;
 90         }
 91 
 92         // Register for HTTP events.
 93         if (Ci.nsITraceableChannel)
 94             httpObserver.addObserver(this, "firebug-http-event", false);
 95     },
 96 
 97     shutdown: function()
 98     {
 99         if (FBTrace.DBG_CACHE)
100             FBTrace.sysout("tabCache.shutdown; Cache model destroyed.");
101 
102         if (Ci.nsITraceableChannel)
103             httpObserver.removeObserver(this, "firebug-http-event");
104     },
105 
106     initContext: function(context)
107     {
108         if (FBTrace.DBG_CACHE)
109             FBTrace.sysout("tabCache.initContext for: " + context.getName());
110     },
111 
112     /* nsIObserver */
113     observe: function(subject, topic, data)
114     {
115         try
116         {
117             if (!(subject instanceof Ci.nsIHttpChannel))
118                 return;
119 // XXXjjb this same code is in net.js, better to have it only once
120             var win = getWindowForRequest(subject);
121             if (win)
122                 var tabId = Firebug.getTabIdForWindow(win);
123             if (!tabId)
124                 return;
125 
126             if (topic == "http-on-modify-request")
127                 this.onModifyRequest(subject, win, tabId);
128             else if (topic == "http-on-examine-response")
129                 this.onExamineResponse(subject, win, tabId);
130             else if (topic == "http-on-examine-cached-response")
131                 this.onCachedResponse(subject, win, tabId);
132         }
133         catch (err)
134         {
135             if (FBTrace.DBG_ERRORS)
136                 FBTrace.sysout("tabCache.observe EXCEPTION", err);
137         }
138     },
139 
140     onModifyRequest: function(request, win, tabId)
141     {
142     },
143 
144     onExamineResponse: function(request, win, tabId)
145     {
146         this.registerStreamListener(request, win);
147     },
148 
149     onCachedResponse: function(request, win, tabId)
150     {
151         this.registerStreamListener(request, win);
152     },
153 
154     registerStreamListener: function(request, win)
155     {
156         try
157         {
158             // Due to #489317, the content type must be checked in onStartRequest
159             //if (!this.shouldCacheRequest(request))
160             //    return;
161 
162             request.QueryInterface(Ci.nsITraceableChannel);
163 
164             // Create Firebug's stream listener that is tracing HTTP responses.
165             var newListener = CCIN("@joehewitt.com/firebug-channel-listener;1", "nsIStreamListener");
166             newListener.wrappedJSObject.window = win;
167 
168             // Set proxy listener for back notifiction from XPCOM to chrome (using real interface
169             // so nsIRequest object is properly passed from XPCOM scope).
170             newListener.QueryInterface(Ci.nsITraceableChannel);
171             newListener.setNewListener(new ChannelListenerProxy(win));
172 
173             // Add tee listener into the chain of request stream listeners so, the chain
174             // doesn't include a JS code. This way all exceptions are propertly distributed
175             // (#515051).
176             // The nsIStreamListenerTee differes in following branches:
177             // 1.9.1: not possible to registere nsIRequestObserver with tee
178             // 1.9.2: implements nsIStreamListenerTee_1_9_2 with initWithObserver methods
179             // 1.9.3: adds third parameter to the existing init method.
180             if (versionChecker.compare(appInfo.version, "3.6*") >= 0)
181             {
182                 var tee = CCIN("@mozilla.org/network/stream-listener-tee;1", "nsIStreamListenerTee");
183                 tee = tee.QueryInterface(Ci.nsIStreamListenerTee);
184 
185                 if (Ci.nsIStreamListenerTee_1_9_2)
186                     tee = tee.QueryInterface(Ci.nsIStreamListenerTee_1_9_2);
187 
188                 // The response will be written into the outputStream of this pipe.
189                 // Both ends of the pipe must be blocking.
190                 var sink = CCIN("@mozilla.org/pipe;1", "nsIPipe");
191                 sink.init(true, true, 0, 0, null);
192 
193                 var originalListener = request.setNewListener(tee);
194                 newListener.wrappedJSObject.sink = sink;
195 
196                 if (tee.initWithObserver)
197                     tee.initWithObserver(originalListener, sink.outputStream, newListener);
198                 else
199                     tee.init(originalListener, sink.outputStream, newListener);
200             }
201             else
202             {
203                 newListener.wrappedJSObject.listener = request.setNewListener(newListener);
204             }
205 
206             // xxxHonza: this is a workaround for #489317. Just passing
207             // shouldCacheRequest method to the component so, onStartRequest
208             // can decide whether to cache or not.
209             newListener.wrappedJSObject.shouldCacheRequest = function(request)
210             {
211                 try {
212                     return Firebug.TabCacheModel.shouldCacheRequest(request)
213                 } catch (err) {}
214                 return false;
215             }
216 
217             // xxxHonza: this is a workaround for the tracing-listener to get the
218             // right context. Notice that if the window (parent browser) is closed
219             // during the response download the TabWatcher (used within this method)
220             // is undefined. But in such a case no cache is needed anyway.
221             // Another thing is that the context isn't available now, but will be
222             // as soon as this method is used from the stream listener.
223             newListener.wrappedJSObject.getContext = function(win)
224             {
225                 try {
226                     return TabWatcher.getContextByWindow(win);
227                 } catch (err){}
228                 return null;
229             }
230         }
231         catch (err)
232         {
233             if (FBTrace.DBG_ERRORS)
234                 FBTrace.sysout("tabCache: Register Traceable Listener EXCEPTION", err);
235         }
236     },
237 
238     shouldCacheRequest: function(request)
239     {
240         request.QueryInterface(Ci.nsIHttpChannel);
241 
242         // Allow to customize caching rules.
243         if (dispatch2(this.fbListeners, "shouldCacheRequest", [request]))
244             return true;
245 
246         // Cache only text responses for now.
247         var contentType = request.contentType;
248         if (contentType)
249             contentType = contentType.split(";")[0];
250 
251         contentType = trim(contentType);
252         if (contentTypes[contentType])
253             return true;
254 
255         // Hack to work around application/octet-stream for js files (2063).
256         // Let's cache all files with js extensions.
257         var extension = getFileExtension(safeGetName(request));
258         if (extension == "js")
259             return true;
260 
261         if (FBTrace.DBG_CACHE)
262             FBTrace.sysout("tabCache.shouldCacheRequest; Request not cached: " +
263                 request.contentType + ", " + safeGetName(request));
264 
265         return false;
266     },
267 });
268 
269 // ************************************************************************************************
270 
271 /**
272  * This cache object is intended to cache all responses made by a specific tab.
273  * The implementation is based on nsITraceableChannel interface introduced in
274  * Firefox 3.0.4. This interface allows to intercept all incoming HTTP data.
275  *
276  * This object replaces the SourceCache, which still exist only for backward
277  * compatibility.
278  *
279  * The object is derived from SourceCache so, the same interface and most of the
280  * implementation is used.
281  */
282 Firebug.TabCache = function(context)
283 {
284     if (FBTrace.DBG_CACHE)
285         FBTrace.sysout("tabCache.TabCache Created for: " + context.getName());
286 
287     Firebug.SourceCache.call(this, context);
288 };
289 
290 Firebug.TabCache.prototype = extend(Firebug.SourceCache.prototype,
291 {
292     responses: [],       // responses in progress.
293 
294     storePartialResponse: function(request, responseText, win)
295     {
296         if (FBTrace.DBG_CACHE)
297             FBTrace.sysout("tabCache.storePartialResponse " + safeGetName(request),
298                 request.contentCharset);
299 
300         try
301         {
302             responseText = FBL.convertToUnicode(responseText, win.document.characterSet);
303         }
304         catch (err)
305         {
306             if (FBTrace.DBG_ERRORS || FBTrace.DBG_CACHE)
307                 FBTrace.sysout("tabCache.storePartialResponse EXCEPTION " +
308                     safeGetName(request), err);
309 
310             // Even responses that are not converted are stored into the cache.
311             // return false;
312         }
313 
314         var url = safeGetName(request);
315         var response = this.getResponse(request);
316 
317         // Size of each response is limited.
318         var limitNotReached = true;
319         if (response.size + responseText.length >= responseSizeLimit)
320         {
321             limitNotReached = false;
322             responseText = responseText.substr(0, responseSizeLimit - response.size);
323             FBTrace.sysout("tabCache.storePartialResponse Max size limit reached for: " + url);
324         }
325 
326         response.size += responseText.length;
327 
328         // Store partial content into the cache.
329         this.store(url, responseText);
330 
331         // Return false if furhter parts of this response should be ignored.
332         return limitNotReached;
333     },
334 
335     getResponse: function(request)
336     {
337         var url = safeGetName(request);
338         var response = this.responses[url];
339         if (!response)
340         {
341             this.invalidate(url);
342             this.responses[url] = response = {
343                 request: request,
344                 size: 0
345             };
346         }
347 
348         return response;
349     },
350 
351     storeSplitLines: function(url, lines)
352     {
353         if (FBTrace.DBG_CACHE)
354             FBTrace.sysout("tabCache.storeSplitLines: " + url, lines);
355 
356         var currLines = this.cache[url];
357         if (!currLines)
358             currLines = this.cache[url] = [];
359 
360         // Join the last line with the new first one so, the source code
361         // lines are properly formatted...
362         if (currLines.length)
363         {
364             // ... but only if the last line isn't already completed.
365             var lastLine = currLines[currLines.length-1];
366             if (lastLine && lastLine.search(/\r|\n/) == -1)
367                 currLines[currLines.length-1] += lines.shift();
368         }
369 
370         // Append new lines (if any) into the array for specified url.
371         if (lines.length)
372             this.cache[url] = currLines.concat(lines);
373 
374         return this.cache[url];
375     },
376 
377     loadFromCache: function(url, method, file)
378     {
379         // The ancestor implementation (SourceCache) uses ioService.newChannel, which
380         // can result in additional request to the server (in case the response can't
381         // be loaded from the Firefox cache) - known as double-load problem.
382         // This new implementation (TabCache) uses nsITraceableChannel so, all responses
383         // should be already cached.
384 
385         // xxxHonza: TODO entire implementation of this method should be removed in Firebug 1.5
386         // xxxHonza: let's try to get the response from the cache till #449198 is fixed.
387         var stream;
388         var responseText;
389         try
390         {
391             if (!url)
392                 return responseText;
393 
394             var channel = ioService.newChannel(url, null, null);
395 
396             // These flag combination doesn't repost the request.
397             channel.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE |
398                 Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
399                 Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
400 
401             var charset = "UTF-8";
402             var doc = this.context.window.document;
403             if (doc)
404                 charset = doc.characterSet;
405 
406             stream = channel.open();
407 
408             // The response doesn't have to be in the browser cache.
409             if (!stream.available())
410             {
411                 if (FBTrace.DBG_CACHE)
412                     FBTrace.sysout("tabCache.loadFromCache; Failed to load source for: " + url);
413 
414                 stream.close();
415                 return [$STR("message.Failed to load source for") + ": " + url];
416             }
417 
418             // Don't load responses that shouldn't be cached.
419             if (!Firebug.TabCacheModel.shouldCacheRequest(channel))
420             {
421                 if (FBTrace.DBG_CACHE)
422                     FBTrace.sysout("tabCache.loadFromCache; The resource from this URL is not text: " + url);
423 
424                 stream.close();
425                 return [$STR("message.The resource from this URL is not text") + ": " + url];
426             }
427 
428             responseText = readFromStream(stream, charset);
429 
430             if (FBTrace.DBG_CACHE)
431                 FBTrace.sysout("tabCache.loadFromCache (response coming from FF Cache) " +
432                     url, responseText);
433 
434             responseText = this.store(url, responseText);
435         }
436         catch (err)
437         {
438             if (FBTrace.DBG_ERRORS || FBTrace.DBG_CACHE)
439                 FBTrace.sysout("tabCache.loadFromCache EXCEPTION on url \'" + url +"\'", err);
440         }
441         finally
442         {
443             if (stream)
444                 stream.close();
445         }
446 
447         return responseText;
448     },
449 
450     // nsIStreamListener - callbacks from channel stream listener component.
451     onStartRequest: function(request, requestContext)
452     {
453         if (FBTrace.DBG_CACHE)
454             FBTrace.sysout("tabCache.channel.startRequest: " + safeGetName(request));
455 
456         // Make sure the response-entry (used to count total response size) is properly
457         // initialized (cleared) now. If no data is received, the response entry remains empty.
458         var response = this.getResponse(request);
459 
460         dispatch(Firebug.TabCacheModel.fbListeners, "onStartRequest", [this.context, request]);
461         dispatch(this.fbListeners, "onStartRequest", [this.context, request]);
462     },
463 
464     onDataAvailable: function(request, requestContext, inputStream, offset, count)
465     {
466         if (FBTrace.DBG_CACHE)
467             FBTrace.sysout("tabCache.channel.onDataAvailable: " + safeGetName(request));
468 
469         // If the stream is read a new one must be provided (the stream doesn't implement
470         // nsISeekableStream).
471         var stream = {
472             value: inputStream
473         };
474 
475         dispatch(Firebug.TabCacheModel.fbListeners, "onDataAvailable",
476             [this.context, request, requestContext, stream, offset, count]);
477         dispatch(this.fbListeners, "onDataAvailable", [this.context,
478             request, requestContext, stream, offset, count]);
479 
480         return stream.value;
481     },
482 
483     onStopRequest: function(request, requestContext, statusCode)
484     {
485         // The response is finally received so, remove the request from the list of
486         // current responses.
487         var url = safeGetName(request);
488         delete this.responses[url];
489 
490         var lines = this.cache[this.removeAnchor(url)];
491         var responseText = lines ? lines.join("") : "";
492 
493         if (FBTrace.DBG_CACHE)
494             FBTrace.sysout("tabCache.channel.stopRequest: " + safeGetRequestName(request), responseText);
495 
496         dispatch(Firebug.TabCacheModel.fbListeners, "onStopRequest", [this.context, request, responseText]);
497         dispatch(this.fbListeners, "onStopRequest", [this.context, request, responseText]);
498     }
499 });
500 
501 // ************************************************************************************************
502 // Proxy Listener
503 
504 function ChannelListenerProxy(win)
505 {
506     this.wrappedJSObject = this;
507     this.window = win;
508 }
509 
510 ChannelListenerProxy.prototype =
511 {
512     /* nsIStreamListener */
513     onStartRequest: function(request, requestContext)
514     {
515         var context = this.getContext();
516         if (context)
517             context.sourceCache.onStartRequest(request, requestContext);
518     },
519 
520     onDataAvailable: function(request, requestContext, inputStream, offset, count)
521     {
522         var context = this.getContext();
523         if (!context)
524             return null;
525 
526         return context.sourceCache.onDataAvailable(request, requestContext, inputStream, offset, count);
527     },
528 
529     onStopRequest: function(request, requestContext, statusCode)
530     {
531         var context = this.getContext();
532         if (context)
533             context.sourceCache.onStopRequest(request, requestContext, statusCode);
534     },
535 
536     /* nsISupports */
537     QueryInterface: function(iid)
538     {
539         if (iid.equals(Ci.nsIStreamListener) ||
540             iid.equals(Ci.nsISupportsWeakReference) ||
541             iid.equals(Ci.nsISupports))
542         {
543             return this;
544         }
545 
546         throw Components.results.NS_NOINTERFACE;
547     },
548 
549     getContext: function()
550     {
551         try {
552             return TabWatcher.getContextByWindow(this.window);
553         } catch (e) {}
554         return null;
555     }
556 }
557 
558 // ************************************************************************************************
559 // Helpers
560 
561 function safeGetName(request)
562 {
563     try {
564         return request.name;
565     }
566     catch (exc) {
567     }
568 
569     return null;
570 }
571 
572 // ************************************************************************************************
573 // Registration
574 
575 Firebug.registerModule(Firebug.TabCacheModel);
576 
577 // ************************************************************************************************
578 
579 }});
580