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