a104035aa9c451008d55ed46577af31fb48d3975
[WebKit-https.git] / Source / WebCore / inspector / front-end / Resource.js
1 /*
2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution.
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28 WebInspector.Resource = function(identifier, url, loaderId)
29 {
30     this.identifier = identifier;
31     this.url = url;
32     this.loaderId = loaderId;
33     this._startTime = -1;
34     this._endTime = -1;
35     this._category = WebInspector.resourceCategories.other;
36     this._pendingContentCallbacks = [];
37     this.history = [];
38 }
39
40 // Keep these in sync with WebCore::InspectorResource::Type
41 WebInspector.Resource.Type = {
42     Document:   0,
43     Stylesheet: 1,
44     Image:      2,
45     Font:       3,
46     Script:     4,
47     XHR:        5,
48     WebSocket:  7,
49     Other:      8,
50
51     isTextType: function(type)
52     {
53         return (type === this.Document) || (type === this.Stylesheet) || (type === this.Script) || (type === this.XHR);
54     },
55
56     toUIString: function(type)
57     {
58         switch (type) {
59             case this.Document:
60                 return WebInspector.UIString("Document");
61             case this.Stylesheet:
62                 return WebInspector.UIString("Stylesheet");
63             case this.Image:
64                 return WebInspector.UIString("Image");
65             case this.Font:
66                 return WebInspector.UIString("Font");
67             case this.Script:
68                 return WebInspector.UIString("Script");
69             case this.XHR:
70                 return WebInspector.UIString("XHR");
71             case this.WebSocket:
72                 return WebInspector.UIString("WebSocket");
73             case this.Other:
74             default:
75                 return WebInspector.UIString("Other");
76         }
77     },
78
79     // Returns locale-independent string identifier of resource type (primarily for use in extension API).
80     // The IDs need to be kept in sync with webInspector.resoureces.Types object in ExtensionAPI.js.
81     toString: function(type)
82     {
83         switch (type) {
84             case this.Document:
85                 return "document";
86             case this.Stylesheet:
87                 return "stylesheet";
88             case this.Image:
89                 return "image";
90             case this.Font:
91                 return "font";
92             case this.Script:
93                 return "script";
94             case this.XHR:
95                 return "xhr";
96             case this.WebSocket:
97                 return "websocket";
98             case this.Other:
99             default:
100                 return "other";
101         }
102     }
103 }
104
105 WebInspector.Resource._domainModelBindings = [];
106
107 WebInspector.Resource.registerDomainModelBinding = function(type, binding)
108 {
109     WebInspector.Resource._domainModelBindings[type] = binding;
110 }
111
112
113 WebInspector.Resource._resourceRevisionRegistry = function()
114 {
115     if (!WebInspector.Resource._resourceRevisionRegistryObject) {
116         if (window.localStorage) {
117             var resourceHistory = window.localStorage["resource-history"];
118             try {
119                 WebInspector.Resource._resourceRevisionRegistryObject = resourceHistory ? JSON.parse(resourceHistory) : {};
120             } catch (e) {
121                 WebInspector.Resource._resourceRevisionRegistryObject = {};
122             }
123         } else
124             WebInspector.Resource._resourceRevisionRegistryObject = {};
125     }
126     return WebInspector.Resource._resourceRevisionRegistryObject;
127 }
128
129 WebInspector.Resource.restoreRevisions = function()
130 {
131     var registry = WebInspector.Resource._resourceRevisionRegistry();
132     var filteredRegistry = {};
133     for (var url in registry) {
134         var historyItems = registry[url];
135         var resource = WebInspector.resourceForURL(url);
136
137         var filteredHistoryItems = [];
138         for (var i = 0; historyItems && i < historyItems.length; ++i) {
139             var historyItem = historyItems[i];
140             if (resource && historyItem.loaderId === resource.loaderId) {
141                 resource.addRevision(window.localStorage[historyItem.key], new Date(historyItem.timestamp), true);
142                 filteredHistoryItems.push(historyItem);
143                 filteredRegistry[url] = filteredHistoryItems;
144             } else
145                 delete window.localStorage[historyItem.key];
146         }
147     }
148     WebInspector.Resource._resourceRevisionRegistryObject = filteredRegistry;
149
150     function persist()
151     {
152         window.localStorage["resource-history"] = JSON.stringify(filteredRegistry);
153     }
154
155     // Schedule async storage.
156     setTimeout(persist, 0);}
157
158 WebInspector.Resource.persistRevision = function(resource)
159 {
160     if (!window.localStorage)
161         return;
162
163     var url = resource.url;
164     var loaderId = resource.loaderId;
165     var timestamp = resource._contentTimestamp.getTime();
166     var key = "resource-history|" + url + "|" + loaderId + "|" + timestamp;
167     var content = resource._content;
168
169     var registry = WebInspector.Resource._resourceRevisionRegistry();
170
171     var historyItems = registry[resource.url];
172     if (!historyItems) {
173         historyItems = [];
174         registry[resource.url] = historyItems;
175     }
176     historyItems.push({url: url, loaderId: loaderId, timestamp: timestamp, key: key});
177
178     function persist()
179     {
180         window.localStorage[key] = content;
181         window.localStorage["resource-history"] = JSON.stringify(registry);
182     }
183
184     // Schedule async storage.
185     setTimeout(persist, 0);
186 }
187
188 WebInspector.Resource.Events = {
189     RevisionAdded: 0
190 }
191
192 WebInspector.Resource.prototype = {
193     get url()
194     {
195         return this._url;
196     },
197
198     set url(x)
199     {
200         if (this._url === x)
201             return;
202
203         this._url = x;
204         delete this._parsedQueryParameters;
205
206         var parsedURL = x.asParsedURL();
207         this.domain = parsedURL ? parsedURL.host : "";
208         this.path = parsedURL ? parsedURL.path : "";
209         this.urlFragment = parsedURL ? parsedURL.fragment : "";
210         this.lastPathComponent = "";
211         if (parsedURL && parsedURL.path) {
212             // First cut the query params.
213             var path = parsedURL.path;
214             var indexOfQuery = path.indexOf("?");
215             if (indexOfQuery !== -1)
216                 path = path.substring(0, indexOfQuery);
217
218             // Then take last path component.
219             var lastSlashIndex = path.lastIndexOf("/");
220             if (lastSlashIndex !== -1)
221                 this.lastPathComponent = path.substring(lastSlashIndex + 1);
222         }
223         this.lastPathComponentLowerCase = this.lastPathComponent.toLowerCase();
224     },
225
226     get documentURL()
227     {
228         return this._documentURL;
229     },
230
231     set documentURL(x)
232     {
233         this._documentURL = x;
234     },
235
236     get displayName()
237     {
238         if (this._displayName)
239             return this._displayName;
240         this._displayName = this.lastPathComponent;
241         if (!this._displayName)
242             this._displayName = this.displayDomain;
243         if (!this._displayName && this.url)
244             this._displayName = this.url.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : "");
245         if (this._displayName === "/")
246             this._displayName = this.url;
247         return this._displayName;
248     },
249
250     get displayDomain()
251     {
252         // WebInspector.Database calls this, so don't access more than this.domain.
253         if (this.domain && (!WebInspector.mainResource || (WebInspector.mainResource && this.domain !== WebInspector.mainResource.domain)))
254             return this.domain;
255         return "";
256     },
257
258     get startTime()
259     {
260         return this._startTime || -1;
261     },
262
263     set startTime(x)
264     {
265         this._startTime = x;
266     },
267
268     get responseReceivedTime()
269     {
270         return this._responseReceivedTime || -1;
271     },
272
273     set responseReceivedTime(x)
274     {
275         this._responseReceivedTime = x;
276     },
277
278     get endTime()
279     {
280         return this._endTime || -1;
281     },
282
283     set endTime(x)
284     {
285         if (this.timing && this.timing.requestTime) {
286             // Check against accurate responseReceivedTime.
287             this._endTime = Math.max(x, this.responseReceivedTime);
288         } else {
289             // Prefer endTime since it might be from the network stack.
290             this._endTime = x;
291             if (this._responseReceivedTime > x)
292                 this._responseReceivedTime = x;
293         }
294     },
295
296     get duration()
297     {
298         if (this._endTime === -1 || this._startTime === -1)
299             return -1;
300         return this._endTime - this._startTime;
301     },
302
303     get latency()
304     {
305         if (this._responseReceivedTime === -1 || this._startTime === -1)
306             return -1;
307         return this._responseReceivedTime - this._startTime;
308     },
309
310     get receiveDuration()
311     {
312         if (this._endTime === -1 || this._responseReceivedTime === -1)
313             return -1;
314         return this._endTime - this._responseReceivedTime;
315     },
316
317     get resourceSize()
318     {
319         return this._resourceSize || 0;
320     },
321
322     set resourceSize(x)
323     {
324         this._resourceSize = x;
325     },
326
327     get transferSize()
328     {
329         if (this.cached)
330             return 0;
331         if (this.statusCode === 304) // Not modified
332             return this.responseHeadersSize;
333         if (this._transferSize !== undefined)
334             return this._transferSize;
335         // If we did not receive actual transfer size from network
336         // stack, we prefer using Content-Length over resourceSize as
337         // resourceSize may differ from actual transfer size if platform's
338         // network stack performed decoding (e.g. gzip decompression).
339         // The Content-Length, though, is expected to come from raw
340         // response headers and will reflect actual transfer length.
341         // This won't work for chunked content encoding, so fall back to
342         // resourceSize when we don't have Content-Length. This still won't
343         // work for chunks with non-trivial encodings. We need a way to
344         // get actual transfer size from the network stack.
345         var bodySize = Number(this.responseHeaders["Content-Length"] || this.resourceSize);
346         return this.responseHeadersSize + bodySize;
347     },
348
349     increaseTransferSize: function(x)
350     {
351         this._transferSize = (this._transferSize || 0) + x;
352     },
353
354     get finished()
355     {
356         return this._finished;
357     },
358
359     set finished(x)
360     {
361         if (this._finished === x)
362             return;
363
364         this._finished = x;
365
366         if (x) {
367             this._checkWarnings();
368             this.dispatchEventToListeners("finished");
369             if (this._pendingContentCallbacks.length)
370                 this._innerRequestContent();
371         }
372     },
373
374     get failed()
375     {
376         return this._failed;
377     },
378
379     set failed(x)
380     {
381         this._failed = x;
382     },
383
384     get canceled()
385     {
386         return this._canceled;
387     },
388
389     set canceled(x)
390     {
391         this._canceled = x;
392     },
393
394     get category()
395     {
396         return this._category;
397     },
398
399     set category(x)
400     {
401         this._category = x;
402     },
403
404     get cached()
405     {
406         return this._cached;
407     },
408
409     set cached(x)
410     {
411         this._cached = x;
412         if (x)
413             delete this._timing;
414     },
415
416     get timing()
417     {
418         return this._timing;
419     },
420
421     set timing(x)
422     {
423         if (x && !this._cached) {
424             // Take startTime and responseReceivedTime from timing data for better accuracy.
425             // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
426             this._startTime = x.requestTime;
427             this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0;
428
429             this._timing = x;
430             this.dispatchEventToListeners("timing changed");
431         }
432     },
433
434     get mimeType()
435     {
436         return this._mimeType;
437     },
438
439     set mimeType(x)
440     {
441         this._mimeType = x;
442     },
443
444     get type()
445     {
446         return this._type;
447     },
448
449     set type(x)
450     {
451         if (this._type === x)
452             return;
453
454         this._type = x;
455
456         switch (x) {
457             case WebInspector.Resource.Type.Document:
458                 this.category = WebInspector.resourceCategories.documents;
459                 break;
460             case WebInspector.Resource.Type.Stylesheet:
461                 this.category = WebInspector.resourceCategories.stylesheets;
462                 break;
463             case WebInspector.Resource.Type.Script:
464                 this.category = WebInspector.resourceCategories.scripts;
465                 break;
466             case WebInspector.Resource.Type.Image:
467                 this.category = WebInspector.resourceCategories.images;
468                 break;
469             case WebInspector.Resource.Type.Font:
470                 this.category = WebInspector.resourceCategories.fonts;
471                 break;
472             case WebInspector.Resource.Type.XHR:
473                 this.category = WebInspector.resourceCategories.xhr;
474                 break;
475             case WebInspector.Resource.Type.WebSocket:
476                 this.category = WebInspector.resourceCategories.websockets;
477                 break;
478             case WebInspector.Resource.Type.Other:
479             default:
480                 this.category = WebInspector.resourceCategories.other;
481                 break;
482         }
483     },
484
485     get requestHeaders()
486     {
487         return this._requestHeaders || {};
488     },
489
490     set requestHeaders(x)
491     {
492         this._requestHeaders = x;
493         delete this._sortedRequestHeaders;
494         delete this._requestCookies;
495
496         this.dispatchEventToListeners("requestHeaders changed");
497     },
498
499     get requestHeadersText()
500     {
501         if (this._requestHeadersText === undefined) {
502             this._requestHeadersText = this.requestMethod + " " + this.url + " HTTP/1.1\r\n";
503             for (var key in this.requestHeaders)
504                 this._requestHeadersText += key + ": " + this.requestHeaders[key] + "\r\n";
505         }
506         return this._requestHeadersText;
507     },
508
509     set requestHeadersText(x)
510     {
511         this._requestHeadersText = x;
512
513         this.dispatchEventToListeners("requestHeaders changed");
514     },
515
516     get requestHeadersSize()
517     {
518         return this.requestHeadersText.length;
519     },
520
521     get sortedRequestHeaders()
522     {
523         if (this._sortedRequestHeaders !== undefined)
524             return this._sortedRequestHeaders;
525
526         this._sortedRequestHeaders = [];
527         for (var key in this.requestHeaders)
528             this._sortedRequestHeaders.push({header: key, value: this.requestHeaders[key]});
529         this._sortedRequestHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
530
531         return this._sortedRequestHeaders;
532     },
533
534     requestHeaderValue: function(headerName)
535     {
536         return this._headerValue(this.requestHeaders, headerName);
537     },
538
539     get requestCookies()
540     {
541         if (!this._requestCookies)
542             this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie"));
543         return this._requestCookies;
544     },
545
546     get requestFormData()
547     {
548         return this._requestFormData;
549     },
550
551     set requestFormData(x)
552     {
553         this._requestFormData = x;
554         delete this._parsedFormParameters;
555     },
556
557     get requestHttpVersion()
558     {
559         var firstLine = this.requestHeadersText.split(/\r\n/)[0];
560         var match = firstLine.match(/(HTTP\/\d+\.\d+)$/);
561         return match ? match[1] : undefined;
562     },
563
564     get responseHeaders()
565     {
566         return this._responseHeaders || {};
567     },
568
569     set responseHeaders(x)
570     {
571         this._responseHeaders = x;
572         delete this._sortedResponseHeaders;
573         delete this._responseCookies;
574
575         this.dispatchEventToListeners("responseHeaders changed");
576     },
577
578     get responseHeadersText()
579     {
580         if (this._responseHeadersText === undefined) {
581             this._responseHeadersText = "HTTP/1.1 " + this.statusCode + " " + this.statusText + "\r\n";
582             for (var key in this.responseHeaders)
583                 this._responseHeadersText += key + ": " + this.responseHeaders[key] + "\r\n";
584         }
585         return this._responseHeadersText;
586     },
587
588     set responseHeadersText(x)
589     {
590         this._responseHeadersText = x;
591
592         this.dispatchEventToListeners("responseHeaders changed");
593     },
594
595     get responseHeadersSize()
596     {
597         return this.responseHeadersText.length;
598     },
599
600     get sortedResponseHeaders()
601     {
602         if (this._sortedResponseHeaders !== undefined)
603             return this._sortedResponseHeaders;
604
605         this._sortedResponseHeaders = [];
606         for (var key in this.responseHeaders)
607             this._sortedResponseHeaders.push({header: key, value: this.responseHeaders[key]});
608         this._sortedResponseHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
609
610         return this._sortedResponseHeaders;
611     },
612
613     responseHeaderValue: function(headerName)
614     {
615         return this._headerValue(this.responseHeaders, headerName);
616     },
617
618     get responseCookies()
619     {
620         if (!this._responseCookies)
621             this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie"));
622         return this._responseCookies;
623     },
624
625     get queryParameters()
626     {
627         if (this._parsedQueryParameters)
628             return this._parsedQueryParameters;
629         var queryString = this.url.split("?", 2)[1];
630         if (!queryString)
631             return;
632         queryString = queryString.split("#", 2)[0];
633         this._parsedQueryParameters = this._parseParameters(queryString);
634         return this._parsedQueryParameters;
635     },
636
637     get formParameters()
638     {
639         if (this._parsedFormParameters)
640             return this._parsedFormParameters;
641         if (!this.requestFormData)
642             return;
643         var requestContentType = this.requestContentType();
644         if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
645             return;
646         this._parsedFormParameters = this._parseParameters(this.requestFormData);
647         return this._parsedFormParameters;
648     },
649
650     get responseHttpVersion()
651     {
652         var match = this.responseHeadersText.match(/^(HTTP\/\d+\.\d+)/);
653         return match ? match[1] : undefined;
654     },
655
656     _parseParameters: function(queryString)
657     {
658         function parseNameValue(pair)
659         {
660             var parameter = {};
661             var splitPair = pair.split("=", 2);
662
663             parameter.name = splitPair[0];
664             if (splitPair.length === 1)
665                 parameter.value = "";
666             else
667                 parameter.value = splitPair[1];
668             return parameter;
669         }
670         return queryString.split("&").map(parseNameValue);
671     },
672
673     _headerValue: function(headers, headerName)
674     {
675         headerName = headerName.toLowerCase();
676         for (var header in headers) {
677             if (header.toLowerCase() === headerName)
678                 return headers[header];
679         }
680     },
681
682     get messages()
683     {
684         return this._messages || [];
685     },
686
687     addMessage: function(msg)
688     {
689         if (!msg.isErrorOrWarning() || !msg.message) 
690             return;
691
692         if (!this._messages)
693             this._messages = [];
694         this._messages.push(msg);
695         this.dispatchEventToListeners("errors-warnings-message-added", msg);
696     },
697
698     get errors()
699     {
700         return this._errors || 0;
701     },
702
703     set errors(x)
704     {
705         this._errors = x;
706     },
707
708     get warnings()
709     {
710         return this._warnings || 0;
711     },
712
713     set warnings(x)
714     {
715         this._warnings = x;
716     },
717
718     clearErrorsAndWarnings: function()
719     {
720         this._messages = [];
721         this._warnings = 0;
722         this._errors = 0;
723         this.dispatchEventToListeners("errors-warnings-cleared");
724     },
725
726     _mimeTypeIsConsistentWithType: function()
727     {
728         // If status is an error, content is likely to be of an inconsistent type,
729         // as it's going to be an error message. We do not want to emit a warning
730         // for this, though, as this will already be reported as resource loading failure.
731         // Also, if a URL like http://localhost/wiki/load.php?debug=true&lang=en produces text/css and gets reloaded,
732         // it is 304 Not Modified and its guessed mime-type is text/php, which is wrong.
733         // Don't check for mime-types in 304-resources.
734         if (this.hasErrorStatusCode() || this.statusCode === 304)
735             return true;
736
737         if (typeof this.type === "undefined"
738             || this.type === WebInspector.Resource.Type.Other
739             || this.type === WebInspector.Resource.Type.XHR
740             || this.type === WebInspector.Resource.Type.WebSocket)
741             return true;
742
743         if (!this.mimeType)
744             return true; // Might be not known for cached resources with null responses.
745
746         if (this.mimeType in WebInspector.MIMETypes)
747             return this.type in WebInspector.MIMETypes[this.mimeType];
748
749         return false;
750     },
751
752     _checkWarnings: function()
753     {
754         for (var warning in WebInspector.Warnings)
755             this._checkWarning(WebInspector.Warnings[warning]);
756     },
757
758     _checkWarning: function(warning)
759     {
760         var msg;
761         switch (warning.id) {
762             case WebInspector.Warnings.IncorrectMIMEType.id:
763                 if (!this._mimeTypeIsConsistentWithType())
764                     msg = new WebInspector.ConsoleMessage(WebInspector.ConsoleMessage.MessageSource.Other,
765                         WebInspector.ConsoleMessage.MessageType.Log,
766                         WebInspector.ConsoleMessage.MessageLevel.Warning,
767                         -1,
768                         this.url,
769                         1,
770                         String.sprintf(WebInspector.Warnings.IncorrectMIMEType.message, WebInspector.Resource.Type.toUIString(this.type), this.mimeType),
771                         null,
772                         null);
773                 break;
774         }
775
776         if (msg)
777             WebInspector.console.addMessage(msg);
778     },
779
780     get content()
781     {
782         return this._content;
783     },
784
785     get contentEncoded()
786     {
787         return this._contentEncoded;
788     },
789
790     get contentTimestamp()
791     {
792         return this._contentTimestamp;
793     },
794
795     setInitialContent: function(content)
796     {
797         this._content = content;
798     },
799
800     isEditable: function()
801     {
802         if (this._actualResource)
803             return false;
804         var binding = WebInspector.Resource._domainModelBindings[this.type];
805         return binding && binding.canSetContent(this);
806     },
807
808     setContent: function(newContent, majorChange, callback)
809     {
810         if (!this.isEditable(this)) {
811             if (callback)
812                 callback("Resource is not editable");
813             return;
814         }
815         var binding = WebInspector.Resource._domainModelBindings[this.type];
816         binding.setContent(this, newContent, majorChange, callback);
817     },
818
819     addRevision: function(newContent, timestamp, restoringHistory)
820     {
821         var revision = new WebInspector.ResourceRevision(this, this._content, this._contentTimestamp);
822         this.history.push(revision);
823
824         this._content = newContent;
825         this._contentTimestamp = timestamp || new Date();
826
827         this.dispatchEventToListeners(WebInspector.Resource.Events.RevisionAdded, revision);
828
829         if (!restoringHistory)
830             this._persistRevision();
831     },
832
833     _persistRevision: function()
834     {
835         WebInspector.Resource.persistRevision(this);
836     },
837
838     requestContent: function(callback)
839     {
840         // We do not support content retrieval for WebSockets at the moment.
841         // Since WebSockets are potentially long-living, fail requests immediately
842         // to prevent caller blocking until resource is marked as finished.
843         if (this.type === WebInspector.Resource.Type.WebSocket) {
844             callback(null, null);
845             return;
846         }
847         if (typeof this._content !== "undefined") {
848             callback(this.content, this._contentEncoded);
849             return;
850         }
851         this._pendingContentCallbacks.push(callback);
852         if (this.finished)
853             this._innerRequestContent();
854     },
855
856     populateImageSource: function(image)
857     {
858         function onResourceContent()
859         {
860             image.src = this._contentURL();
861         }
862
863         if (Preferences.useDataURLForResourceImageIcons)
864             this.requestContent(onResourceContent.bind(this));
865         else
866             image.src = this.url;
867     },
868
869     isDataURL: function()
870     {
871         return this.url.match(/^data:/i);
872     },
873
874     requestContentType: function()
875     {
876         return this.requestHeaderValue("Content-Type");
877     },
878
879     isPingRequest: function()
880     {
881         return "text/ping" === this.requestContentType();
882     },
883
884     hasErrorStatusCode: function()
885     {
886         return this.statusCode >= 400;
887     },
888
889     _contentURL: function()
890     {
891         const maxDataUrlSize = 1024 * 1024;
892         // If resource content is not available or won't fit a data URL, fall back to using original URL.
893         if (this._content == null || this._content.length > maxDataUrlSize)
894             return this.url;
895
896         return "data:" + this.mimeType + (this._contentEncoded ? ";base64," : ",") + this._content;
897     },
898
899     _innerRequestContent: function()
900     {
901         if (this._contentRequested)
902             return;
903         this._contentRequested = true;
904
905         function onResourceContent(data, contentEncoded)
906         {
907             this._contentEncoded = contentEncoded;
908             this._content = data;
909             this._originalContent = data;
910             var callbacks = this._pendingContentCallbacks.slice();
911             for (var i = 0; i < callbacks.length; ++i)
912                 callbacks[i](this._content, this._contentEncoded);
913             this._pendingContentCallbacks.length = 0;
914             delete this._contentRequested;
915         }
916         WebInspector.networkManager.requestContent(this, onResourceContent.bind(this));
917     }
918 }
919
920 WebInspector.Resource.prototype.__proto__ = WebInspector.Object.prototype;
921
922 WebInspector.ResourceRevision = function(resource, content, timestamp)
923 {
924     this._resource = resource;
925     this._content = content;
926     this._timestamp = timestamp;
927 }
928
929 WebInspector.ResourceRevision.prototype = {
930     get resource()
931     {
932         return this._resource;
933     },
934
935     get timestamp()
936     {
937         return this._timestamp;
938     },
939
940     get content()
941     {
942         return this._content;
943     },
944
945     revertToThis: function()
946     {
947         function revert(content)
948         {
949             this._resource.setContent(content, true);
950         }
951         this.requestContent(revert.bind(this));
952     },
953
954     requestContent: function(callback)
955     {
956         if (typeof this._content === "string") {
957             callback(this._content);
958             return;
959         }
960
961         // If we are here, this is initial revision. First, look up content fetched over the wire.
962         if (typeof this.resource._originalContent === "string") {
963             this._content = this._resource._originalContent;
964             callback(this._content);
965             return;
966         }
967
968         // If unsuccessful, request the content.
969         function mycallback(content)
970         {
971             this._content = content;
972             callback(content);
973         }
974         WebInspector.networkManager.requestContent(this._resource, mycallback.bind(this));
975     }
976 }
977
978 WebInspector.ResourceDomainModelBinding = function()
979 {
980 }
981
982 WebInspector.ResourceDomainModelBinding.prototype = {
983     canSetContent: function()
984     {
985         // Implemented by the domains.
986         return true;
987     },
988
989     setContent: function(resource, content, majorChange, callback)
990     {
991         // Implemented by the domains.
992     }
993 }