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