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