Home | History | Annotate | Download | only in front-end
      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)
     29 {
     30     this.identifier = identifier;
     31     this.url = url;
     32     this._startTime = -1;
     33     this._endTime = -1;
     34     this._category = WebInspector.resourceCategories.other;
     35     this._pendingContentCallbacks = [];
     36     this.history = [];
     37 }
     38 
     39 // Keep these in sync with WebCore::InspectorResource::Type
     40 WebInspector.Resource.Type = {
     41     Document:   0,
     42     Stylesheet: 1,
     43     Image:      2,
     44     Font:       3,
     45     Script:     4,
     46     XHR:        5,
     47     WebSocket:  7,
     48     Other:      8,
     49 
     50     isTextType: function(type)
     51     {
     52         return (type === this.Document) || (type === this.Stylesheet) || (type === this.Script) || (type === this.XHR);
     53     },
     54 
     55     toUIString: function(type)
     56     {
     57         switch (type) {
     58             case this.Document:
     59                 return WebInspector.UIString("Document");
     60             case this.Stylesheet:
     61                 return WebInspector.UIString("Stylesheet");
     62             case this.Image:
     63                 return WebInspector.UIString("Image");
     64             case this.Font:
     65                 return WebInspector.UIString("Font");
     66             case this.Script:
     67                 return WebInspector.UIString("Script");
     68             case this.XHR:
     69                 return WebInspector.UIString("XHR");
     70             case this.WebSocket:
     71                 return WebInspector.UIString("WebSocket");
     72             case this.Other:
     73             default:
     74                 return WebInspector.UIString("Other");
     75         }
     76     },
     77 
     78     // Returns locale-independent string identifier of resource type (primarily for use in extension API).
     79     // The IDs need to be kept in sync with webInspector.resoureces.Types object in ExtensionAPI.js.
     80     toString: function(type)
     81     {
     82         switch (type) {
     83             case this.Document:
     84                 return "document";
     85             case this.Stylesheet:
     86                 return "stylesheet";
     87             case this.Image:
     88                 return "image";
     89             case this.Font:
     90                 return "font";
     91             case this.Script:
     92                 return "script";
     93             case this.XHR:
     94                 return "xhr";
     95             case this.WebSocket:
     96                 return "websocket";
     97             case this.Other:
     98             default:
     99                 return "other";
    100         }
    101     }
    102 }
    103 
    104 WebInspector.Resource._domainModelBindings = [];
    105 
    106 WebInspector.Resource.registerDomainModelBinding = function(type, binding)
    107 {
    108     WebInspector.Resource._domainModelBindings[type] = binding;
    109 }
    110 
    111 WebInspector.Resource.Events = {
    112     RevisionAdded: 0
    113 }
    114 
    115 WebInspector.Resource.prototype = {
    116     get url()
    117     {
    118         return this._url;
    119     },
    120 
    121     set url(x)
    122     {
    123         if (this._url === x)
    124             return;
    125 
    126         this._url = x;
    127         delete this._parsedQueryParameters;
    128 
    129         var parsedURL = x.asParsedURL();
    130         this.domain = parsedURL ? parsedURL.host : "";
    131         this.path = parsedURL ? parsedURL.path : "";
    132         this.lastPathComponent = "";
    133         if (parsedURL && parsedURL.path) {
    134             // First cut the query params.
    135             var path = parsedURL.path;
    136             var indexOfQuery = path.indexOf("?");
    137             if (indexOfQuery !== -1)
    138                 path = path.substring(0, indexOfQuery);
    139 
    140             // Then take last path component.
    141             var lastSlashIndex = path.lastIndexOf("/");
    142             if (lastSlashIndex !== -1)
    143                 this.lastPathComponent = path.substring(lastSlashIndex + 1);
    144         }
    145         this.lastPathComponentLowerCase = this.lastPathComponent.toLowerCase();
    146     },
    147 
    148     get documentURL()
    149     {
    150         return this._documentURL;
    151     },
    152 
    153     set documentURL(x)
    154     {
    155         this._documentURL = x;
    156     },
    157 
    158     get displayName()
    159     {
    160         if (this._displayName)
    161             return this._displayName;
    162         this._displayName = this.lastPathComponent;
    163         if (!this._displayName)
    164             this._displayName = this.displayDomain;
    165         if (!this._displayName && this.url)
    166             this._displayName = this.url.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : "");
    167         if (this._displayName === "/")
    168             this._displayName = this.url;
    169         return this._displayName;
    170     },
    171 
    172     get displayDomain()
    173     {
    174         // WebInspector.Database calls this, so don't access more than this.domain.
    175         if (this.domain && (!WebInspector.mainResource || (WebInspector.mainResource && this.domain !== WebInspector.mainResource.domain)))
    176             return this.domain;
    177         return "";
    178     },
    179 
    180     get startTime()
    181     {
    182         return this._startTime || -1;
    183     },
    184 
    185     set startTime(x)
    186     {
    187         this._startTime = x;
    188     },
    189 
    190     get responseReceivedTime()
    191     {
    192         return this._responseReceivedTime || -1;
    193     },
    194 
    195     set responseReceivedTime(x)
    196     {
    197         this._responseReceivedTime = x;
    198     },
    199 
    200     get endTime()
    201     {
    202         return this._endTime || -1;
    203     },
    204 
    205     set endTime(x)
    206     {
    207         if (this.timing && this.timing.requestTime) {
    208             // Check against accurate responseReceivedTime.
    209             this._endTime = Math.max(x, this.responseReceivedTime);
    210         } else {
    211             // Prefer endTime since it might be from the network stack.
    212             this._endTime = x;
    213             if (this._responseReceivedTime > x)
    214                 this._responseReceivedTime = x;
    215         }
    216     },
    217 
    218     get duration()
    219     {
    220         if (this._endTime === -1 || this._startTime === -1)
    221             return -1;
    222         return this._endTime - this._startTime;
    223     },
    224 
    225     get latency()
    226     {
    227         if (this._responseReceivedTime === -1 || this._startTime === -1)
    228             return -1;
    229         return this._responseReceivedTime - this._startTime;
    230     },
    231 
    232     get receiveDuration()
    233     {
    234         if (this._endTime === -1 || this._responseReceivedTime === -1)
    235             return -1;
    236         return this._endTime - this._responseReceivedTime;
    237     },
    238 
    239     get resourceSize()
    240     {
    241         return this._resourceSize || 0;
    242     },
    243 
    244     set resourceSize(x)
    245     {
    246         this._resourceSize = x;
    247     },
    248 
    249     get transferSize()
    250     {
    251         if (this.cached)
    252             return 0;
    253         if (this.statusCode === 304) // Not modified
    254             return this.responseHeadersSize;
    255         if (this._transferSize !== undefined)
    256             return this._transferSize;
    257         // If we did not receive actual transfer size from network
    258         // stack, we prefer using Content-Length over resourceSize as
    259         // resourceSize may differ from actual transfer size if platform's
    260         // network stack performed decoding (e.g. gzip decompression).
    261         // The Content-Length, though, is expected to come from raw
    262         // response headers and will reflect actual transfer length.
    263         // This won't work for chunked content encoding, so fall back to
    264         // resourceSize when we don't have Content-Length. This still won't
    265         // work for chunks with non-trivial encodings. We need a way to
    266         // get actual transfer size from the network stack.
    267         var bodySize = Number(this.responseHeaders["Content-Length"] || this.resourceSize);
    268         return this.responseHeadersSize + bodySize;
    269     },
    270 
    271     increaseTransferSize: function(x)
    272     {
    273         this._transferSize = (this._transferSize || 0) + x;
    274     },
    275 
    276     get finished()
    277     {
    278         return this._finished;
    279     },
    280 
    281     set finished(x)
    282     {
    283         if (this._finished === x)
    284             return;
    285 
    286         this._finished = x;
    287 
    288         if (x) {
    289             this._checkWarnings();
    290             this.dispatchEventToListeners("finished");
    291             if (this._pendingContentCallbacks.length)
    292                 this._innerRequestContent();
    293         }
    294     },
    295 
    296     get failed()
    297     {
    298         return this._failed;
    299     },
    300 
    301     set failed(x)
    302     {
    303         this._failed = x;
    304     },
    305 
    306     get canceled()
    307     {
    308         return this._canceled;
    309     },
    310 
    311     set canceled(x)
    312     {
    313         this._canceled = x;
    314     },
    315 
    316     get category()
    317     {
    318         return this._category;
    319     },
    320 
    321     set category(x)
    322     {
    323         this._category = x;
    324     },
    325 
    326     get cached()
    327     {
    328         return this._cached;
    329     },
    330 
    331     set cached(x)
    332     {
    333         this._cached = x;
    334         if (x)
    335             delete this._timing;
    336     },
    337 
    338     get timing()
    339     {
    340         return this._timing;
    341     },
    342 
    343     set timing(x)
    344     {
    345         if (x && !this._cached) {
    346             // Take startTime and responseReceivedTime from timing data for better accuracy.
    347             // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
    348             this._startTime = x.requestTime;
    349             this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0;
    350 
    351             this._timing = x;
    352             this.dispatchEventToListeners("timing changed");
    353         }
    354     },
    355 
    356     get mimeType()
    357     {
    358         return this._mimeType;
    359     },
    360 
    361     set mimeType(x)
    362     {
    363         this._mimeType = x;
    364     },
    365 
    366     get type()
    367     {
    368         return this._type;
    369     },
    370 
    371     set type(x)
    372     {
    373         if (this._type === x)
    374             return;
    375 
    376         this._type = x;
    377 
    378         switch (x) {
    379             case WebInspector.Resource.Type.Document:
    380                 this.category = WebInspector.resourceCategories.documents;
    381                 break;
    382             case WebInspector.Resource.Type.Stylesheet:
    383                 this.category = WebInspector.resourceCategories.stylesheets;
    384                 break;
    385             case WebInspector.Resource.Type.Script:
    386                 this.category = WebInspector.resourceCategories.scripts;
    387                 break;
    388             case WebInspector.Resource.Type.Image:
    389                 this.category = WebInspector.resourceCategories.images;
    390                 break;
    391             case WebInspector.Resource.Type.Font:
    392                 this.category = WebInspector.resourceCategories.fonts;
    393                 break;
    394             case WebInspector.Resource.Type.XHR:
    395                 this.category = WebInspector.resourceCategories.xhr;
    396                 break;
    397             case WebInspector.Resource.Type.WebSocket:
    398                 this.category = WebInspector.resourceCategories.websockets;
    399                 break;
    400             case WebInspector.Resource.Type.Other:
    401             default:
    402                 this.category = WebInspector.resourceCategories.other;
    403                 break;
    404         }
    405     },
    406 
    407     get requestHeaders()
    408     {
    409         return this._requestHeaders || {};
    410     },
    411 
    412     set requestHeaders(x)
    413     {
    414         this._requestHeaders = x;
    415         delete this._sortedRequestHeaders;
    416         delete this._requestCookies;
    417         delete this._responseHeadersSize;
    418 
    419         this.dispatchEventToListeners("requestHeaders changed");
    420     },
    421 
    422     get requestHeadersText()
    423     {
    424         return this._requestHeadersText;
    425     },
    426 
    427     set requestHeadersText(x)
    428     {
    429         this._requestHeadersText = x;
    430         delete this._responseHeadersSize;
    431 
    432         this.dispatchEventToListeners("requestHeaders changed");
    433     },
    434 
    435     get requestHeadersSize()
    436     {
    437         if (typeof(this._requestHeadersSize) === "undefined") {
    438             if (this._requestHeadersText)
    439                 this._requestHeadersSize = this._requestHeadersText.length;
    440             else
    441                 this._requestHeadersSize = this._headersSize(this._requestHeaders)
    442         }
    443         return this._requestHeadersSize;
    444     },
    445 
    446     get sortedRequestHeaders()
    447     {
    448         if (this._sortedRequestHeaders !== undefined)
    449             return this._sortedRequestHeaders;
    450 
    451         this._sortedRequestHeaders = [];
    452         for (var key in this.requestHeaders)
    453             this._sortedRequestHeaders.push({header: key, value: this.requestHeaders[key]});
    454         this._sortedRequestHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
    455 
    456         return this._sortedRequestHeaders;
    457     },
    458 
    459     requestHeaderValue: function(headerName)
    460     {
    461         return this._headerValue(this.requestHeaders, headerName);
    462     },
    463 
    464     get requestCookies()
    465     {
    466         if (!this._requestCookies)
    467             this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie"));
    468         return this._requestCookies;
    469     },
    470 
    471     get requestFormData()
    472     {
    473         return this._requestFormData;
    474     },
    475 
    476     set requestFormData(x)
    477     {
    478         this._requestFormData = x;
    479         delete this._parsedFormParameters;
    480     },
    481 
    482     get responseHeaders()
    483     {
    484         return this._responseHeaders || {};
    485     },
    486 
    487     set responseHeaders(x)
    488     {
    489         this._responseHeaders = x;
    490         delete this._responseHeadersSize;
    491         delete this._sortedResponseHeaders;
    492         delete this._responseCookies;
    493 
    494         this.dispatchEventToListeners("responseHeaders changed");
    495     },
    496 
    497     get responseHeadersText()
    498     {
    499         return this._responseHeadersText;
    500     },
    501 
    502     set responseHeadersText(x)
    503     {
    504         this._responseHeadersText = x;
    505         delete this._responseHeadersSize;
    506 
    507         this.dispatchEventToListeners("responseHeaders changed");
    508     },
    509 
    510     get responseHeadersSize()
    511     {
    512         if (typeof(this._responseHeadersSize) === "undefined") {
    513             if (this._responseHeadersText)
    514                 this._responseHeadersSize = this._responseHeadersText.length;
    515             else
    516                 this._responseHeadersSize = this._headersSize(this._responseHeaders)
    517         }
    518         return this._responseHeadersSize;
    519     },
    520 
    521 
    522     get sortedResponseHeaders()
    523     {
    524         if (this._sortedResponseHeaders !== undefined)
    525             return this._sortedResponseHeaders;
    526 
    527         this._sortedResponseHeaders = [];
    528         for (var key in this.responseHeaders)
    529             this._sortedResponseHeaders.push({header: key, value: this.responseHeaders[key]});
    530         this._sortedResponseHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
    531 
    532         return this._sortedResponseHeaders;
    533     },
    534 
    535     responseHeaderValue: function(headerName)
    536     {
    537         return this._headerValue(this.responseHeaders, headerName);
    538     },
    539 
    540     get responseCookies()
    541     {
    542         if (!this._responseCookies)
    543             this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie"));
    544         return this._responseCookies;
    545     },
    546 
    547     get queryParameters()
    548     {
    549         if (this._parsedQueryParameters)
    550             return this._parsedQueryParameters;
    551         var queryString = this.url.split("?", 2)[1];
    552         if (!queryString)
    553             return;
    554         this._parsedQueryParameters = this._parseParameters(queryString);
    555         return this._parsedQueryParameters;
    556     },
    557 
    558     get formParameters()
    559     {
    560         if (this._parsedFormParameters)
    561             return this._parsedFormParameters;
    562         if (!this.requestFormData)
    563             return;
    564         var requestContentType = this.requestHeaderValue("Content-Type");
    565         if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
    566             return;
    567         this._parsedFormParameters = this._parseParameters(this.requestFormData);
    568         return this._parsedFormParameters;
    569     },
    570 
    571     _parseParameters: function(queryString)
    572     {
    573         function parseNameValue(pair)
    574         {
    575             var parameter = {};
    576             var splitPair = pair.split("=", 2);
    577 
    578             parameter.name = splitPair[0];
    579             if (splitPair.length === 1)
    580                 parameter.value = "";
    581             else
    582                 parameter.value = splitPair[1];
    583             return parameter;
    584         }
    585         return queryString.split("&").map(parseNameValue);
    586     },
    587 
    588     _headerValue: function(headers, headerName)
    589     {
    590         headerName = headerName.toLowerCase();
    591         for (var header in headers) {
    592             if (header.toLowerCase() === headerName)
    593                 return headers[header];
    594         }
    595     },
    596 
    597     _headersSize: function(headers)
    598     {
    599         // We should take actual headers size from network stack, when possible, but fall back to
    600         // this lousy computation when no headers text is available.
    601         var size = 0;
    602         for (var header in headers)
    603             size += header.length + headers[header].length + 4; // _typical_ overhead per header is ": ".length + "\r\n".length.
    604         return size;
    605     },
    606 
    607     get errors()
    608     {
    609         return this._errors || 0;
    610     },
    611 
    612     set errors(x)
    613     {
    614         this._errors = x;
    615         this.dispatchEventToListeners("errors-warnings-updated");
    616     },
    617 
    618     get warnings()
    619     {
    620         return this._warnings || 0;
    621     },
    622 
    623     set warnings(x)
    624     {
    625         this._warnings = x;
    626         this.dispatchEventToListeners("errors-warnings-updated");
    627     },
    628 
    629     clearErrorsAndWarnings: function()
    630     {
    631         this._warnings = 0;
    632         this._errors = 0;
    633         this.dispatchEventToListeners("errors-warnings-updated");
    634     },
    635 
    636     _mimeTypeIsConsistentWithType: function()
    637     {
    638         // If status is an error, content is likely to be of an inconsistent type,
    639         // as it's going to be an error message. We do not want to emit a warning
    640         // for this, though, as this will already be reported as resource loading failure.
    641         // Also, if a URL like http://localhost/wiki/load.php?debug=true&lang=en produces text/css and gets reloaded,
    642         // it is 304 Not Modified and its guessed mime-type is text/php, which is wrong.
    643         // Don't check for mime-types in 304-resources.
    644         if (this.statusCode >= 400 || this.statusCode === 304)
    645             return true;
    646 
    647         if (typeof this.type === "undefined"
    648             || this.type === WebInspector.Resource.Type.Other
    649             || this.type === WebInspector.Resource.Type.XHR
    650             || this.type === WebInspector.Resource.Type.WebSocket)
    651             return true;
    652 
    653         if (!this.mimeType)
    654             return true; // Might be not known for cached resources with null responses.
    655 
    656         if (this.mimeType in WebInspector.MIMETypes)
    657             return this.type in WebInspector.MIMETypes[this.mimeType];
    658 
    659         return false;
    660     },
    661 
    662     _checkWarnings: function()
    663     {
    664         for (var warning in WebInspector.Warnings)
    665             this._checkWarning(WebInspector.Warnings[warning]);
    666     },
    667 
    668     _checkWarning: function(warning)
    669     {
    670         var msg;
    671         switch (warning.id) {
    672             case WebInspector.Warnings.IncorrectMIMEType.id:
    673                 if (!this._mimeTypeIsConsistentWithType())
    674                     msg = new WebInspector.ConsoleMessage(WebInspector.ConsoleMessage.MessageSource.Other,
    675                         WebInspector.ConsoleMessage.MessageType.Log,
    676                         WebInspector.ConsoleMessage.MessageLevel.Warning,
    677                         -1,
    678                         this.url,
    679                         1,
    680                         String.sprintf(WebInspector.Warnings.IncorrectMIMEType.message, WebInspector.Resource.Type.toUIString(this.type), this.mimeType),
    681                         null,
    682                         null);
    683                 break;
    684         }
    685 
    686         if (msg)
    687             WebInspector.console.addMessage(msg);
    688     },
    689 
    690     get content()
    691     {
    692         return this._content;
    693     },
    694 
    695     get contentTimestamp()
    696     {
    697         return this._contentTimestamp;
    698     },
    699 
    700     setInitialContent: function(content)
    701     {
    702         this._content = content;
    703     },
    704 
    705     isEditable: function()
    706     {
    707         if (this._actualResource)
    708             return false;
    709         var binding = WebInspector.Resource._domainModelBindings[this.type];
    710         return binding && binding.canSetContent(this);
    711     },
    712 
    713     setContent: function(newContent, majorChange, callback)
    714     {
    715         if (!this.isEditable(this)) {
    716             if (callback)
    717                 callback("Resource is not editable");
    718             return;
    719         }
    720         var binding = WebInspector.Resource._domainModelBindings[this.type];
    721         binding.setContent(this, newContent, majorChange, callback);
    722     },
    723 
    724     addRevision: function(newContent)
    725     {
    726         var revision = new WebInspector.ResourceRevision(this, this._content, this._contentTimestamp);
    727         this.history.push(revision);
    728 
    729         this._content = newContent;
    730         this._contentTimestamp = new Date();
    731 
    732         this.dispatchEventToListeners(WebInspector.Resource.Events.RevisionAdded, revision);
    733     },
    734 
    735     requestContent: function(callback)
    736     {
    737         // We do not support content retrieval for WebSockets at the moment.
    738         // Since WebSockets are potentially long-living, fail requests immediately
    739         // to prevent caller blocking until resource is marked as finished.
    740         if (this.type === WebInspector.Resource.Type.WebSocket) {
    741             callback(null, null);
    742             return;
    743         }
    744         if (typeof this._content !== "undefined") {
    745             callback(this.content, this._contentEncoded);
    746             return;
    747         }
    748         this._pendingContentCallbacks.push(callback);
    749         if (this.finished)
    750             this._innerRequestContent();
    751     },
    752 
    753     populateImageSource: function(image)
    754     {
    755         function onResourceContent()
    756         {
    757             image.src = this._contentURL();
    758         }
    759 
    760         if (Preferences.useDataURLForResourceImageIcons)
    761             this.requestContent(onResourceContent.bind(this));
    762         else
    763             image.src = this.url;
    764     },
    765 
    766     isDataURL: function()
    767     {
    768         return this.url.match(/^data:/i);
    769     },
    770 
    771     _contentURL: function()
    772     {
    773         const maxDataUrlSize = 1024 * 1024;
    774         // If resource content is not available or won't fit a data URL, fall back to using original URL.
    775         if (this._content == null || this._content.length > maxDataUrlSize)
    776             return this.url;
    777 
    778         return "data:" + this.mimeType + (this._contentEncoded ? ";base64," : ",") + this._content;
    779     },
    780 
    781     _innerRequestContent: function()
    782     {
    783         if (this._contentRequested)
    784             return;
    785         this._contentRequested = true;
    786         this._contentEncoded = !WebInspector.Resource.Type.isTextType(this.type);
    787 
    788         function onResourceContent(data)
    789         {
    790             this._content = data;
    791             this._originalContent = data;
    792             var callbacks = this._pendingContentCallbacks.slice();
    793             for (var i = 0; i < callbacks.length; ++i)
    794                 callbacks[i](this._content, this._contentEncoded);
    795             this._pendingContentCallbacks.length = 0;
    796             delete this._contentRequested;
    797         }
    798         WebInspector.networkManager.requestContent(this, this._contentEncoded, onResourceContent.bind(this));
    799     }
    800 }
    801 
    802 WebInspector.Resource.prototype.__proto__ = WebInspector.Object.prototype;
    803 
    804 WebInspector.ResourceRevision = function(resource, content, timestamp)
    805 {
    806     this._resource = resource;
    807     this._content = content;
    808     this._timestamp = timestamp;
    809 }
    810 
    811 WebInspector.ResourceRevision.prototype = {
    812     get resource()
    813     {
    814         return this._resource;
    815     },
    816 
    817     get timestamp()
    818     {
    819         return this._timestamp;
    820     },
    821 
    822     get content()
    823     {
    824         return this._content;
    825     },
    826 
    827     revertToThis: function()
    828     {
    829         function revert(content)
    830         {
    831             this._resource.setContent(content, true);
    832         }
    833         this.requestContent(revert.bind(this));
    834     },
    835 
    836     requestContent: function(callback)
    837     {
    838         if (typeof this._content === "string") {
    839             callback(this._content);
    840             return;
    841         }
    842 
    843         // If we are here, this is initial revision. First, look up content fetched over the wire.
    844         if (typeof this.resource._originalContent === "string") {
    845             this._content = this._resource._originalContent;
    846             callback(this._content);
    847             return;
    848         }
    849 
    850         // If unsuccessful, request the content.
    851         function mycallback(content)
    852         {
    853             this._content = content;
    854             callback(content);
    855         }
    856         WebInspector.networkManager.requestContent(this._resource, false, mycallback.bind(this));
    857     }
    858 }
    859 
    860 WebInspector.ResourceDomainModelBinding = function()
    861 {
    862 }
    863 
    864 WebInspector.ResourceDomainModelBinding.prototype = {
    865     canSetContent: function()
    866     {
    867         // Implemented by the domains.
    868         return true;
    869     },
    870 
    871     setContent: function(resource, content, majorChange, callback)
    872     {
    873         // Implemented by the domains.
    874     }
    875 }
    876