Web Inspector: NetworkPanel search failed if the matched sting is in the query part...
[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 folder()
251     {
252         var path = this.path;
253         var indexOfQuery = path.indexOf("?");
254         if (indexOfQuery !== -1)
255             path = path.substring(0, indexOfQuery);
256         var lastSlashIndex = path.lastIndexOf("/");
257         return lastSlashIndex !== -1 ? path.substring(0, lastSlashIndex) : "";
258     },
259
260     get displayDomain()
261     {
262         // WebInspector.Database calls this, so don't access more than this.domain.
263         if (this.domain && (!WebInspector.mainResource || (WebInspector.mainResource && this.domain !== WebInspector.mainResource.domain)))
264             return this.domain;
265         return "";
266     },
267
268     get startTime()
269     {
270         return this._startTime || -1;
271     },
272
273     set startTime(x)
274     {
275         this._startTime = x;
276     },
277
278     get responseReceivedTime()
279     {
280         return this._responseReceivedTime || -1;
281     },
282
283     set responseReceivedTime(x)
284     {
285         this._responseReceivedTime = x;
286     },
287
288     get endTime()
289     {
290         return this._endTime || -1;
291     },
292
293     set endTime(x)
294     {
295         if (this.timing && this.timing.requestTime) {
296             // Check against accurate responseReceivedTime.
297             this._endTime = Math.max(x, this.responseReceivedTime);
298         } else {
299             // Prefer endTime since it might be from the network stack.
300             this._endTime = x;
301             if (this._responseReceivedTime > x)
302                 this._responseReceivedTime = x;
303         }
304     },
305
306     get duration()
307     {
308         if (this._endTime === -1 || this._startTime === -1)
309             return -1;
310         return this._endTime - this._startTime;
311     },
312
313     get latency()
314     {
315         if (this._responseReceivedTime === -1 || this._startTime === -1)
316             return -1;
317         return this._responseReceivedTime - this._startTime;
318     },
319
320     get receiveDuration()
321     {
322         if (this._endTime === -1 || this._responseReceivedTime === -1)
323             return -1;
324         return this._endTime - this._responseReceivedTime;
325     },
326
327     get resourceSize()
328     {
329         return this._resourceSize || 0;
330     },
331
332     set resourceSize(x)
333     {
334         this._resourceSize = x;
335     },
336
337     get transferSize()
338     {
339         if (this.cached)
340             return 0;
341         if (this.statusCode === 304) // Not modified
342             return this.responseHeadersSize;
343         if (this._transferSize !== undefined)
344             return this._transferSize;
345         // If we did not receive actual transfer size from network
346         // stack, we prefer using Content-Length over resourceSize as
347         // resourceSize may differ from actual transfer size if platform's
348         // network stack performed decoding (e.g. gzip decompression).
349         // The Content-Length, though, is expected to come from raw
350         // response headers and will reflect actual transfer length.
351         // This won't work for chunked content encoding, so fall back to
352         // resourceSize when we don't have Content-Length. This still won't
353         // work for chunks with non-trivial encodings. We need a way to
354         // get actual transfer size from the network stack.
355         var bodySize = Number(this.responseHeaders["Content-Length"] || this.resourceSize);
356         return this.responseHeadersSize + bodySize;
357     },
358
359     increaseTransferSize: function(x)
360     {
361         this._transferSize = (this._transferSize || 0) + x;
362     },
363
364     get finished()
365     {
366         return this._finished;
367     },
368
369     set finished(x)
370     {
371         if (this._finished === x)
372             return;
373
374         this._finished = x;
375
376         if (x) {
377             this._checkWarnings();
378             this.dispatchEventToListeners("finished");
379             if (this._pendingContentCallbacks.length)
380                 this._innerRequestContent();
381         }
382     },
383
384     get failed()
385     {
386         return this._failed;
387     },
388
389     set failed(x)
390     {
391         this._failed = x;
392     },
393
394     get canceled()
395     {
396         return this._canceled;
397     },
398
399     set canceled(x)
400     {
401         this._canceled = x;
402     },
403
404     get category()
405     {
406         return this._category;
407     },
408
409     set category(x)
410     {
411         this._category = x;
412     },
413
414     get cached()
415     {
416         return this._cached;
417     },
418
419     set cached(x)
420     {
421         this._cached = x;
422         if (x)
423             delete this._timing;
424     },
425
426     get timing()
427     {
428         return this._timing;
429     },
430
431     set timing(x)
432     {
433         if (x && !this._cached) {
434             // Take startTime and responseReceivedTime from timing data for better accuracy.
435             // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
436             this._startTime = x.requestTime;
437             this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0;
438
439             this._timing = x;
440             this.dispatchEventToListeners("timing changed");
441         }
442     },
443
444     get mimeType()
445     {
446         return this._mimeType;
447     },
448
449     set mimeType(x)
450     {
451         this._mimeType = x;
452     },
453
454     get type()
455     {
456         return this._type;
457     },
458
459     set type(x)
460     {
461         if (this._type === x)
462             return;
463
464         this._type = x;
465
466         switch (x) {
467             case WebInspector.Resource.Type.Document:
468                 this.category = WebInspector.resourceCategories.documents;
469                 break;
470             case WebInspector.Resource.Type.Stylesheet:
471                 this.category = WebInspector.resourceCategories.stylesheets;
472                 break;
473             case WebInspector.Resource.Type.Script:
474                 this.category = WebInspector.resourceCategories.scripts;
475                 break;
476             case WebInspector.Resource.Type.Image:
477                 this.category = WebInspector.resourceCategories.images;
478                 break;
479             case WebInspector.Resource.Type.Font:
480                 this.category = WebInspector.resourceCategories.fonts;
481                 break;
482             case WebInspector.Resource.Type.XHR:
483                 this.category = WebInspector.resourceCategories.xhr;
484                 break;
485             case WebInspector.Resource.Type.WebSocket:
486                 this.category = WebInspector.resourceCategories.websockets;
487                 break;
488             case WebInspector.Resource.Type.Other:
489             default:
490                 this.category = WebInspector.resourceCategories.other;
491                 break;
492         }
493     },
494
495     get requestHeaders()
496     {
497         return this._requestHeaders || {};
498     },
499
500     set requestHeaders(x)
501     {
502         this._requestHeaders = x;
503         delete this._sortedRequestHeaders;
504         delete this._requestCookies;
505
506         this.dispatchEventToListeners("requestHeaders changed");
507     },
508
509     get requestHeadersText()
510     {
511         if (this._requestHeadersText === undefined) {
512             this._requestHeadersText = this.requestMethod + " " + this.url + " HTTP/1.1\r\n";
513             for (var key in this.requestHeaders)
514                 this._requestHeadersText += key + ": " + this.requestHeaders[key] + "\r\n";
515         }
516         return this._requestHeadersText;
517     },
518
519     set requestHeadersText(x)
520     {
521         this._requestHeadersText = x;
522
523         this.dispatchEventToListeners("requestHeaders changed");
524     },
525
526     get requestHeadersSize()
527     {
528         return this.requestHeadersText.length;
529     },
530
531     get sortedRequestHeaders()
532     {
533         if (this._sortedRequestHeaders !== undefined)
534             return this._sortedRequestHeaders;
535
536         this._sortedRequestHeaders = [];
537         for (var key in this.requestHeaders)
538             this._sortedRequestHeaders.push({header: key, value: this.requestHeaders[key]});
539         this._sortedRequestHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
540
541         return this._sortedRequestHeaders;
542     },
543
544     requestHeaderValue: function(headerName)
545     {
546         return this._headerValue(this.requestHeaders, headerName);
547     },
548
549     get requestCookies()
550     {
551         if (!this._requestCookies)
552             this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie"));
553         return this._requestCookies;
554     },
555
556     get requestFormData()
557     {
558         return this._requestFormData;
559     },
560
561     set requestFormData(x)
562     {
563         this._requestFormData = x;
564         delete this._parsedFormParameters;
565     },
566
567     get requestHttpVersion()
568     {
569         var firstLine = this.requestHeadersText.split(/\r\n/)[0];
570         var match = firstLine.match(/(HTTP\/\d+\.\d+)$/);
571         return match ? match[1] : undefined;
572     },
573
574     get responseHeaders()
575     {
576         return this._responseHeaders || {};
577     },
578
579     set responseHeaders(x)
580     {
581         this._responseHeaders = x;
582         delete this._sortedResponseHeaders;
583         delete this._responseCookies;
584
585         this.dispatchEventToListeners("responseHeaders changed");
586     },
587
588     get responseHeadersText()
589     {
590         if (this._responseHeadersText === undefined) {
591             this._responseHeadersText = "HTTP/1.1 " + this.statusCode + " " + this.statusText + "\r\n";
592             for (var key in this.responseHeaders)
593                 this._responseHeadersText += key + ": " + this.responseHeaders[key] + "\r\n";
594         }
595         return this._responseHeadersText;
596     },
597
598     set responseHeadersText(x)
599     {
600         this._responseHeadersText = x;
601
602         this.dispatchEventToListeners("responseHeaders changed");
603     },
604
605     get responseHeadersSize()
606     {
607         return this.responseHeadersText.length;
608     },
609
610     get sortedResponseHeaders()
611     {
612         if (this._sortedResponseHeaders !== undefined)
613             return this._sortedResponseHeaders;
614
615         this._sortedResponseHeaders = [];
616         for (var key in this.responseHeaders)
617             this._sortedResponseHeaders.push({header: key, value: this.responseHeaders[key]});
618         this._sortedResponseHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
619
620         return this._sortedResponseHeaders;
621     },
622
623     responseHeaderValue: function(headerName)
624     {
625         return this._headerValue(this.responseHeaders, headerName);
626     },
627
628     get responseCookies()
629     {
630         if (!this._responseCookies)
631             this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie"));
632         return this._responseCookies;
633     },
634
635     get queryParameters()
636     {
637         if (this._parsedQueryParameters)
638             return this._parsedQueryParameters;
639         var queryString = this.url.split("?", 2)[1];
640         if (!queryString)
641             return;
642         queryString = queryString.split("#", 2)[0];
643         this._parsedQueryParameters = this._parseParameters(queryString);
644         return this._parsedQueryParameters;
645     },
646
647     get formParameters()
648     {
649         if (this._parsedFormParameters)
650             return this._parsedFormParameters;
651         if (!this.requestFormData)
652             return;
653         var requestContentType = this.requestContentType();
654         if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
655             return;
656         this._parsedFormParameters = this._parseParameters(this.requestFormData);
657         return this._parsedFormParameters;
658     },
659
660     get responseHttpVersion()
661     {
662         var match = this.responseHeadersText.match(/^(HTTP\/\d+\.\d+)/);
663         return match ? match[1] : undefined;
664     },
665
666     _parseParameters: function(queryString)
667     {
668         function parseNameValue(pair)
669         {
670             var parameter = {};
671             var splitPair = pair.split("=", 2);
672
673             parameter.name = splitPair[0];
674             if (splitPair.length === 1)
675                 parameter.value = "";
676             else
677                 parameter.value = splitPair[1];
678             return parameter;
679         }
680         return queryString.split("&").map(parseNameValue);
681     },
682
683     _headerValue: function(headers, headerName)
684     {
685         headerName = headerName.toLowerCase();
686         for (var header in headers) {
687             if (header.toLowerCase() === headerName)
688                 return headers[header];
689         }
690     },
691
692     get messages()
693     {
694         return this._messages || [];
695     },
696
697     addMessage: function(msg)
698     {
699         if (!msg.isErrorOrWarning() || !msg.message) 
700             return;
701
702         if (!this._messages)
703             this._messages = [];
704         this._messages.push(msg);
705         this.dispatchEventToListeners("errors-warnings-message-added", msg);
706     },
707
708     get errors()
709     {
710         return this._errors || 0;
711     },
712
713     set errors(x)
714     {
715         this._errors = x;
716     },
717
718     get warnings()
719     {
720         return this._warnings || 0;
721     },
722
723     set warnings(x)
724     {
725         this._warnings = x;
726     },
727
728     clearErrorsAndWarnings: function()
729     {
730         this._messages = [];
731         this._warnings = 0;
732         this._errors = 0;
733         this.dispatchEventToListeners("errors-warnings-cleared");
734     },
735
736     _mimeTypeIsConsistentWithType: function()
737     {
738         // If status is an error, content is likely to be of an inconsistent type,
739         // as it's going to be an error message. We do not want to emit a warning
740         // for this, though, as this will already be reported as resource loading failure.
741         // Also, if a URL like http://localhost/wiki/load.php?debug=true&lang=en produces text/css and gets reloaded,
742         // it is 304 Not Modified and its guessed mime-type is text/php, which is wrong.
743         // Don't check for mime-types in 304-resources.
744         if (this.hasErrorStatusCode() || this.statusCode === 304)
745             return true;
746
747         if (typeof this.type === "undefined"
748             || this.type === WebInspector.Resource.Type.Other
749             || this.type === WebInspector.Resource.Type.XHR
750             || this.type === WebInspector.Resource.Type.WebSocket)
751             return true;
752
753         if (!this.mimeType)
754             return true; // Might be not known for cached resources with null responses.
755
756         if (this.mimeType in WebInspector.MIMETypes)
757             return this.type in WebInspector.MIMETypes[this.mimeType];
758
759         return false;
760     },
761
762     _checkWarnings: function()
763     {
764         for (var warning in WebInspector.Warnings)
765             this._checkWarning(WebInspector.Warnings[warning]);
766     },
767
768     _checkWarning: function(warning)
769     {
770         var msg;
771         switch (warning.id) {
772             case WebInspector.Warnings.IncorrectMIMEType.id:
773                 if (!this._mimeTypeIsConsistentWithType())
774                     msg = new WebInspector.ConsoleMessage(WebInspector.ConsoleMessage.MessageSource.Other,
775                         WebInspector.ConsoleMessage.MessageType.Log,
776                         WebInspector.ConsoleMessage.MessageLevel.Warning,
777                         -1,
778                         this.url,
779                         1,
780                         String.sprintf(WebInspector.Warnings.IncorrectMIMEType.message, WebInspector.Resource.Type.toUIString(this.type), this.mimeType),
781                         null,
782                         null);
783                 break;
784         }
785
786         if (msg)
787             WebInspector.console.addMessage(msg);
788     },
789
790     get content()
791     {
792         return this._content;
793     },
794
795     get contentEncoded()
796     {
797         return this._contentEncoded;
798     },
799
800     get contentTimestamp()
801     {
802         return this._contentTimestamp;
803     },
804
805     setInitialContent: function(content)
806     {
807         this._content = content;
808     },
809
810     isEditable: function()
811     {
812         if (this._actualResource)
813             return false;
814         var binding = WebInspector.Resource._domainModelBindings[this.type];
815         return binding && binding.canSetContent(this);
816     },
817
818     setContent: function(newContent, majorChange, callback)
819     {
820         if (!this.isEditable(this)) {
821             if (callback)
822                 callback("Resource is not editable");
823             return;
824         }
825         var binding = WebInspector.Resource._domainModelBindings[this.type];
826         binding.setContent(this, newContent, majorChange, callback);
827     },
828
829     addRevision: function(newContent, timestamp, restoringHistory)
830     {
831         var revision = new WebInspector.ResourceRevision(this, this._content, this._contentTimestamp);
832         this.history.push(revision);
833
834         this._content = newContent;
835         this._contentTimestamp = timestamp || new Date();
836
837         this.dispatchEventToListeners(WebInspector.Resource.Events.RevisionAdded, revision);
838
839         if (!restoringHistory)
840             this._persistRevision();
841     },
842
843     _persistRevision: function()
844     {
845         WebInspector.Resource.persistRevision(this);
846     },
847
848     requestContent: function(callback)
849     {
850         // We do not support content retrieval for WebSockets at the moment.
851         // Since WebSockets are potentially long-living, fail requests immediately
852         // to prevent caller blocking until resource is marked as finished.
853         if (this.type === WebInspector.Resource.Type.WebSocket) {
854             callback(null, null);
855             return;
856         }
857         if (typeof this._content !== "undefined") {
858             callback(this.content, this._contentEncoded);
859             return;
860         }
861         this._pendingContentCallbacks.push(callback);
862         if (this.finished)
863             this._innerRequestContent();
864     },
865
866     populateImageSource: function(image)
867     {
868         function onResourceContent()
869         {
870             image.src = this._contentURL();
871         }
872
873         if (Preferences.useDataURLForResourceImageIcons)
874             this.requestContent(onResourceContent.bind(this));
875         else
876             image.src = this.url;
877     },
878
879     isDataURL: function()
880     {
881         return this.url.match(/^data:/i);
882     },
883
884     requestContentType: function()
885     {
886         return this.requestHeaderValue("Content-Type");
887     },
888
889     isPingRequest: function()
890     {
891         return "text/ping" === this.requestContentType();
892     },
893
894     hasErrorStatusCode: function()
895     {
896         return this.statusCode >= 400;
897     },
898
899     _contentURL: function()
900     {
901         const maxDataUrlSize = 1024 * 1024;
902         // If resource content is not available or won't fit a data URL, fall back to using original URL.
903         if (this._content == null || this._content.length > maxDataUrlSize)
904             return this.url;
905
906         return "data:" + this.mimeType + (this._contentEncoded ? ";base64," : ",") + this._content;
907     },
908
909     _innerRequestContent: function()
910     {
911         if (this._contentRequested)
912             return;
913         this._contentRequested = true;
914
915         function onResourceContent(data, contentEncoded)
916         {
917             this._contentEncoded = contentEncoded;
918             this._content = data;
919             this._originalContent = data;
920             var callbacks = this._pendingContentCallbacks.slice();
921             for (var i = 0; i < callbacks.length; ++i)
922                 callbacks[i](this._content, this._contentEncoded);
923             this._pendingContentCallbacks.length = 0;
924             delete this._contentRequested;
925         }
926         WebInspector.networkManager.requestContent(this, onResourceContent.bind(this));
927     }
928 }
929
930 WebInspector.Resource.prototype.__proto__ = WebInspector.Object.prototype;
931
932 WebInspector.ResourceRevision = function(resource, content, timestamp)
933 {
934     this._resource = resource;
935     this._content = content;
936     this._timestamp = timestamp;
937 }
938
939 WebInspector.ResourceRevision.prototype = {
940     get resource()
941     {
942         return this._resource;
943     },
944
945     get timestamp()
946     {
947         return this._timestamp;
948     },
949
950     get content()
951     {
952         return this._content;
953     },
954
955     revertToThis: function()
956     {
957         function revert(content)
958         {
959             this._resource.setContent(content, true);
960         }
961         this.requestContent(revert.bind(this));
962     },
963
964     requestContent: function(callback)
965     {
966         if (typeof this._content === "string") {
967             callback(this._content);
968             return;
969         }
970
971         // If we are here, this is initial revision. First, look up content fetched over the wire.
972         if (typeof this.resource._originalContent === "string") {
973             this._content = this._resource._originalContent;
974             callback(this._content);
975             return;
976         }
977
978         // If unsuccessful, request the content.
979         function mycallback(content)
980         {
981             this._content = content;
982             callback(content);
983         }
984         WebInspector.networkManager.requestContent(this._resource, mycallback.bind(this));
985     }
986 }
987
988 WebInspector.ResourceDomainModelBinding = function()
989 {
990 }
991
992 WebInspector.ResourceDomainModelBinding.prototype = {
993     canSetContent: function()
994     {
995         // Implemented by the domains.
996         return true;
997     },
998
999     setContent: function(resource, content, majorChange, callback)
1000     {
1001         // Implemented by the domains.
1002     }
1003 }