/* * Copyright (C) 2013 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ WebInspector.Resource = function(url, mimeType, type, loaderIdentifier, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, initiatorSourceCodeLocation) { WebInspector.SourceCode.call(this); console.assert(url); if (type in WebInspector.Resource.Type) type = WebInspector.Resource.Type[type]; this._url = url; this._mimeType = mimeType; this._type = type || WebInspector.Resource.Type.fromMIMEType(mimeType); this._loaderIdentifier = loaderIdentifier || null; this._requestIdentifier = requestIdentifier || null; this._requestMethod = requestMethod || null; this._requestData = requestData || null; this._requestHeaders = requestHeaders || {}; this._responseHeaders = {}; this._parentFrame = null; this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null; this._requestSentTimestamp = requestSentTimestamp || NaN; this._responseReceivedTimestamp = NaN; this._lastRedirectReceivedTimestamp = NaN; this._lastDataReceivedTimestamp = NaN; this._finishedOrFailedTimestamp = NaN; this._size = NaN; this._transferSize = NaN; this._cached = false; }; WebInspector.Object.addConstructorFunctions(WebInspector.Resource); WebInspector.Resource.TypeIdentifier = "resource"; WebInspector.Resource.URLCookieKey = "resource-url"; WebInspector.Resource.MainResourceCookieKey = "resource-is-main-resource"; WebInspector.Resource.Event = { URLDidChange: "resource-url-did-change", MIMETypeDidChange: "resource-mime-type-did-change", TypeDidChange: "resource-type-did-change", RequestHeadersDidChange: "resource-request-headers-did-change", ResponseReceived: "resource-response-received", LoadingDidFinish: "resource-loading-did-finish", LoadingDidFail: "resource-loading-did-fail", TimestampsDidChange: "resource-timestamps-did-change", SizeDidChange: "resource-size-did-change", TransferSizeDidChange: "resource-transfer-size-did-change", CacheStatusDidChange: "resource-cached-did-change" }; // Keep these in sync with the "ResourceType" enum defined by the "Page" domain. WebInspector.Resource.Type = { Document: "resource-type-document", Stylesheet: "resource-type-stylesheet", Image: "resource-type-image", Font: "resource-type-font", Script: "resource-type-script", XHR: "resource-type-xhr", WebSocket: "resource-type-websocket", Other: "resource-type-other" }; // This MIME Type map is private, use WebInspector.Resource.Type.fromMIMEType(). WebInspector.Resource.Type._mimeTypeMap = { "text/html": WebInspector.Resource.Type.Document, "text/xml": WebInspector.Resource.Type.Document, "text/plain": WebInspector.Resource.Type.Document, "application/xhtml+xml": WebInspector.Resource.Type.Document, "image/svg+xml": WebInspector.Resource.Type.Document, "text/css": WebInspector.Resource.Type.Stylesheet, "text/xsl": WebInspector.Resource.Type.Stylesheet, "text/x-less": WebInspector.Resource.Type.Stylesheet, "text/x-sass": WebInspector.Resource.Type.Stylesheet, "text/x-scss": WebInspector.Resource.Type.Stylesheet, "application/pdf": WebInspector.Resource.Type.Image, "application/x-font-type1": WebInspector.Resource.Type.Font, "application/x-font-ttf": WebInspector.Resource.Type.Font, "application/x-font-woff": WebInspector.Resource.Type.Font, "application/x-truetype-font": WebInspector.Resource.Type.Font, "text/javascript": WebInspector.Resource.Type.Script, "text/ecmascript": WebInspector.Resource.Type.Script, "application/javascript": WebInspector.Resource.Type.Script, "application/ecmascript": WebInspector.Resource.Type.Script, "application/x-javascript": WebInspector.Resource.Type.Script, "application/json": WebInspector.Resource.Type.Script, "application/x-json": WebInspector.Resource.Type.Script, "text/x-javascript": WebInspector.Resource.Type.Script, "text/x-json": WebInspector.Resource.Type.Script, "text/javascript1.1": WebInspector.Resource.Type.Script, "text/javascript1.2": WebInspector.Resource.Type.Script, "text/javascript1.3": WebInspector.Resource.Type.Script, "text/jscript": WebInspector.Resource.Type.Script, "text/livescript": WebInspector.Resource.Type.Script, "text/x-livescript": WebInspector.Resource.Type.Script, "text/typescript": WebInspector.Resource.Type.Script, "text/x-clojure": WebInspector.Resource.Type.Script, "text/x-coffeescript": WebInspector.Resource.Type.Script }; WebInspector.Resource.Type.fromMIMEType = function(mimeType) { if (!mimeType) return WebInspector.Resource.Type.Other; mimeType = parseMIMEType(mimeType).type; if (mimeType in WebInspector.Resource.Type._mimeTypeMap) return WebInspector.Resource.Type._mimeTypeMap[mimeType]; if (mimeType.startsWith("image/")) return WebInspector.Resource.Type.Image; if (mimeType.startsWith("font/")) return WebInspector.Resource.Type.Font; return WebInspector.Resource.Type.Other; }; WebInspector.Resource.Type.displayName = function(type, plural) { switch(type) { case WebInspector.Resource.Type.Document: if (plural) return WebInspector.UIString("Documents"); return WebInspector.UIString("Document"); case WebInspector.Resource.Type.Stylesheet: if (plural) return WebInspector.UIString("Stylesheets"); return WebInspector.UIString("Stylesheet"); case WebInspector.Resource.Type.Image: if (plural) return WebInspector.UIString("Images"); return WebInspector.UIString("Image"); case WebInspector.Resource.Type.Font: if (plural) return WebInspector.UIString("Fonts"); return WebInspector.UIString("Font"); case WebInspector.Resource.Type.Script: if (plural) return WebInspector.UIString("Scripts"); return WebInspector.UIString("Script"); case WebInspector.Resource.Type.XHR: if (plural) return WebInspector.UIString("XHRs"); return WebInspector.UIString("XHR"); case WebInspector.Resource.Type.WebSocket: if (plural) return WebInspector.UIString("Sockets"); return WebInspector.UIString("Socket"); case WebInspector.Resource.Type.Other: return WebInspector.UIString("Other"); default: console.error("Unknown resource type: ", type); return null; } }; WebInspector.Resource.prototype = { constructor: WebInspector.Resource, // Public get url() { return this._url; }, get urlComponents() { if (!this._urlComponents) this._urlComponents = parseURL(this._url); return this._urlComponents; }, get displayName() { return WebInspector.displayNameForURL(this._url, this.urlComponents); }, get initiatorSourceCodeLocation() { return this._initiatorSourceCodeLocation; }, get type() { return this._type; }, get mimeType() { return this._mimeType; }, get mimeTypeComponents() { if (!this._mimeTypeComponents) this._mimeTypeComponents = parseMIMEType(this._mimeType); return this._mimeTypeComponents; }, get syntheticMIMEType() { // Resources are often transferred with a MIME-type that doesn't match the purpose the // resource was loaded for, which is what WebInspector.Resource.Type represents. // This getter generates a MIME-type, if needed, that matches the resource type. // If the type matches the Resource.Type of the MIME-type, then return the actual MIME-type. if (this._type === WebInspector.Resource.Type.fromMIMEType(this._mimeType)) return this._mimeType; // Return the default MIME-types for the Resource.Type, since the current MIME-type // does not match what is expected for the Resource.Type. switch (this._type) { case WebInspector.Resource.Type.Document: return "text/html"; case WebInspector.Resource.Type.Stylesheet: return "text/css"; case WebInspector.Resource.Type.Script: return "text/javascript"; } // Return the actual MIME-type since we don't have a better synthesized one to return. return this._mimeType; }, get contentURL() { const maximumDataURLSize = 1024 * 1024; // 1 MiB // If content is not available or won't fit a data URL, fallback to using original URL. var content = this.content; if (content === null || content.length > maximumDataURLSize) return this._url; return "data:" + this.mimeTypeComponents.type + (this.contentIsBase64Encoded ? ";base64," + content : "," + encodeURIComponent(content)); }, isMainResource: function() { return this._parentFrame ? this._parentFrame.mainResource === this : false; }, get parentFrame() { return this._parentFrame; }, get loaderIdentifier() { return this._loaderIdentifier; }, get requestIdentifier() { return this._requestIdentifier; }, get finished() { return this._finished; }, get failed() { return this._failed; }, get canceled() { return this._canceled; }, get requestMethod() { return this._requestMethod; }, get requestData() { return this._requestData; }, get requestDataContentType() { return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null; }, get requestHeaders() { return this._requestHeaders; }, get responseHeaders() { return this._responseHeaders; }, get requestSentTimestamp() { return this._requestSentTimestamp; }, get lastRedirectReceivedTimestamp() { return this._lastRedirectReceivedTimestamp; }, get responseReceivedTimestamp() { return this._responseReceivedTimestamp; }, get lastDataReceivedTimestamp() { return this._lastDataReceivedTimestamp; }, get finishedOrFailedTimestamp() { return this._finishedOrFailedTimestamp; }, get firstTimestamp() { return this.requestSentTimestamp || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp; }, get lastTimestamp() { return this.finishedOrFailedTimestamp || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp; }, get duration() { return this._finishedOrFailedTimestamp - this._requestSentTimestamp; }, get latency() { return this._responseReceivedTimestamp - this._requestSentTimestamp; }, get receiveDuration() { return this._finishedOrFailedTimestamp - this._responseReceivedTimestamp; }, get cached() { return this._cached; }, get statusCode() { return this._statusCode; }, get statusText() { return this._statusText; }, get size() { return this._size; }, get encodedSize() { if (!isNaN(this._transferSize)) return this._transferSize; // If we did not receive actual transfer size from network // stack, we prefer using Content-Length over resourceSize as // resourceSize may differ from actual transfer size if platform's // network stack performed decoding (e.g. gzip decompression). // The Content-Length, though, is expected to come from raw // response headers and will reflect actual transfer length. // This won't work for chunked content encoding, so fall back to // resourceSize when we don't have Content-Length. This still won't // work for chunks with non-trivial encodings. We need a way to // get actual transfer size from the network stack. return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._size); }, get transferSize() { if (this.statusCode === 304) // Not modified return this._responseHeadersSize; if (this._cached) return 0; return this._responseHeadersSize + this.encodedSize; }, get compressed() { var contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding"); return contentEncoding && /\b(?:gzip|deflate)\b/.test(contentEncoding); }, get scripts() { return this._scripts || []; }, scriptForLocation: function(sourceCodeLocation) { console.assert(!(this instanceof WebInspector.SourceMapResource)); console.assert(sourceCodeLocation.sourceCode === this, "SourceCodeLocation must be in this Resource"); if (sourceCodeLocation.sourceCode !== this) return null; var lineNumber = sourceCodeLocation.lineNumber; var columnNumber = sourceCodeLocation.columnNumber; for (var i = 0; i < this._scripts.length; ++i) { var script = this._scripts[i]; if (script.range.startLine <= lineNumber && script.range.endLine >= lineNumber) { if (script.range.startLine === lineNumber && columnNumber < script.range.startColumn) continue; if (script.range.endLine === lineNumber && columnNumber > script.range.endColumn) continue; return script; } } return null; }, updateForRedirectResponse: function(url, requestHeaders, timestamp) { console.assert(!this._finished); console.assert(!this._failed); console.assert(!this._canceled); var oldURL = this._url; this._url = url; this._requestHeaders = requestHeaders || {}; this._lastRedirectReceivedTimestamp = timestamp || NaN; if (oldURL !== url) { // Delete the URL components so the URL is re-parsed the next time it is requested. delete this._urlComponents; this.dispatchEventToListeners(WebInspector.Resource.Event.URLDidChange, {oldURL: oldURL}); } this.dispatchEventToListeners(WebInspector.Resource.Event.RequestHeadersDidChange); this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange); }, updateForResponse: function(url, mimeType, type, responseHeaders, statusCode, statusText, timestamp) { console.assert(!this._finished); console.assert(!this._failed); console.assert(!this._canceled); var oldURL = this._url; var oldMIMEType = this._mimeType; var oldType = this._type; if (type in WebInspector.Resource.Type) type = WebInspector.Resource.Type[type]; this._url = url; this._mimeType = mimeType; this._type = type || WebInspector.Resource.Type.fromMIMEType(mimeType); this._statusCode = statusCode; this._statusText = statusText; this._responseHeaders = responseHeaders || {}; this._responseReceivedTimestamp = timestamp || NaN; this._responseHeadersSize = String(this._statusCode).length + this._statusText.length + 12; // Extra length is for "HTTP/1.1 ", " ", and "\r\n". for (var name in this._responseHeaders) this._responseHeadersSize += name.length + this._responseHeaders[name].length + 4; // Extra length is for ": ", and "\r\n". if (statusCode === 304 && !this._cached) this.markAsCached(); if (oldURL !== url) { // Delete the URL components so the URL is re-parsed the next time it is requested. delete this._urlComponents; this.dispatchEventToListeners(WebInspector.Resource.Event.URLDidChange, {oldURL: oldURL}); } if (oldMIMEType !== mimeType) { // Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested. delete this._mimeTypeComponents; this.dispatchEventToListeners(WebInspector.Resource.Event.MIMETypeDidChange, {oldMIMEType: oldMIMEType}); } if (oldType !== type) this.dispatchEventToListeners(WebInspector.Resource.Event.TypeDidChange, {oldType: oldType}); console.assert(isNaN(this._size)); console.assert(isNaN(this._transferSize)); // The transferSize becomes 0 when status is 304 or Content-Length is available, so // notify listeners of that change. if (statusCode === 304 || this._responseHeaders.valueForCaseInsensitiveKey("Content-Length")) this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange); this.dispatchEventToListeners(WebInspector.Resource.Event.ResponseReceived); this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange); }, canRequestContentFromBackend: function() { return this._finished; }, requestContentFromBackend: function(callback) { // If we have the requestIdentifier we can get the actual response for this specific resource. // Otherwise the content will be cached resource data, which might not exist anymore. if (this._requestIdentifier) { NetworkAgent.getResponseBody(this._requestIdentifier, callback); return true; } if (this._parentFrame) { PageAgent.getResourceContent(this._parentFrame.id, this._url, callback); return true; } // There is no request identifier or frame to request content from. Return false to cause the // pending callbacks to get null content. return false; }, increaseSize: function(dataLength, timestamp) { console.assert(dataLength >= 0); if (isNaN(this._size)) this._size = 0; var previousSize = this._size; this._size += dataLength; this._lastDataReceivedTimestamp = timestamp || NaN; this.dispatchEventToListeners(WebInspector.Resource.Event.SizeDidChange, {previousSize: previousSize}); // The transferSize is based off of size when status is not 304 or Content-Length is missing. if (isNaN(this._transferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length")) this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange); }, increaseTransferSize: function(encodedDataLength) { console.assert(encodedDataLength >= 0); if (isNaN(this._transferSize)) this._transferSize = 0; this._transferSize += encodedDataLength; this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange); }, markAsCached: function() { this._cached = true; this.dispatchEventToListeners(WebInspector.Resource.Event.CacheStatusDidChange); // The transferSize is starts returning 0 when cached is true, unless status is 304. if (this._statusCode !== 304) this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange); }, markAsFinished: function(timestamp) { console.assert(!this._failed); console.assert(!this._canceled); this._finished = true; this._finishedOrFailedTimestamp = timestamp || NaN; this.dispatchEventToListeners(WebInspector.Resource.Event.LoadingDidFinish); this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange); if (this.canRequestContentFromBackend()) this.requestContentFromBackendIfNeeded(); }, markAsFailed: function(canceled, timestamp) { console.assert(!this._finished); this._failed = true; this._canceled = canceled; this._finishedOrFailedTimestamp = timestamp || NaN; this.dispatchEventToListeners(WebInspector.Resource.Event.LoadingDidFail); this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange); // Force the content requests to be serviced. They will get null as the content. this.servicePendingContentRequests(true); }, revertMarkAsFinished: function(timestamp) { console.assert(!this._failed); console.assert(!this._canceled); console.assert(this._finished); this._finished = false; this._finishedOrFailedTimestamp = NaN; }, getImageSize: function(callback) { // Throw an error in the case this resource is not an image. if (this.type !== WebInspector.Resource.Type.Image) throw "Resource is not an image."; // See if we've already computed and cached the image size, // in which case we can provide them directly. if (this._imageSize) { callback(this._imageSize); return; } // Event handler for the image "load" event. function imageDidLoad() { // Cache the image metrics. this._imageSize = { width: image.width, height: image.height }; callback(this._imageSize); }; // Create an element that we'll use to load the image resource // so that we can query its intrinsic size. var image = new Image; image.addEventListener("load", imageDidLoad.bind(this), false); // Set the image source once we've obtained the base64-encoded URL for it. this.requestContent(function() { image.src = this.contentURL; }.bind(this)); }, associateWithScript: function(script) { if (!this._scripts) this._scripts = [] this._scripts.push(script); // COMPATIBILITY (iOS 6): Resources did not know their type until a response // was received. We can set the Resource type to be Script here. if (this._type === WebInspector.Resource.Type.Other) { var oldType = this._type; this._type = WebInspector.Resource.Type.Script; this.dispatchEventToListeners(WebInspector.Resource.Event.TypeDidChange, {oldType: oldType}); } }, saveIdentityToCookie: function(cookie) { cookie[WebInspector.Resource.URLCookieKey] = this.url; cookie[WebInspector.Resource.MainResourceCookieKey] = this.isMainResource(); } }; WebInspector.Resource.prototype.__proto__ = WebInspector.SourceCode.prototype;