| /* |
| * Copyright (C) 2013 Apple Inc. All rights reserved. |
| * Copyright (C) 2011 Google 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. |
| */ |
| |
| WI.Resource = class Resource extends WI.SourceCode |
| { |
| constructor(url, mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, requestSentWalltime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp) |
| { |
| super(); |
| |
| console.assert(url); |
| |
| if (type in WI.Resource.Type) |
| type = WI.Resource.Type[type]; |
| |
| this._url = url; |
| this._urlComponents = null; |
| this._mimeType = mimeType; |
| this._mimeTypeComponents = null; |
| this._type = Resource.resolvedType(type, mimeType); |
| this._loaderIdentifier = loaderIdentifier || null; |
| this._requestIdentifier = requestIdentifier || null; |
| this._queryStringParameters = undefined; |
| this._requestFormParameters = undefined; |
| this._requestMethod = requestMethod || null; |
| this._requestData = requestData || null; |
| this._requestHeaders = requestHeaders || {}; |
| this._responseHeaders = {}; |
| this._requestCookies = null; |
| this._responseCookies = null; |
| this._parentFrame = null; |
| this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null; |
| this._initiatedResources = []; |
| this._originalRequestWillBeSentTimestamp = originalRequestWillBeSentTimestamp || null; |
| this._requestSentTimestamp = requestSentTimestamp || NaN; |
| this._requestSentWalltime = requestSentWalltime || NaN; |
| this._responseReceivedTimestamp = NaN; |
| this._lastRedirectReceivedTimestamp = NaN; |
| this._lastDataReceivedTimestamp = NaN; |
| this._finishedOrFailedTimestamp = NaN; |
| this._finishThenRequestContentPromise = null; |
| this._statusCode = NaN; |
| this._statusText = null; |
| this._cached = false; |
| this._canceled = false; |
| this._failed = false; |
| this._failureReasonText = null; |
| this._receivedNetworkLoadMetrics = false; |
| this._responseSource = WI.Resource.ResponseSource.Unknown; |
| this._timingData = new WI.ResourceTimingData(this); |
| this._protocol = null; |
| this._priority = WI.Resource.NetworkPriority.Unknown; |
| this._remoteAddress = null; |
| this._connectionIdentifier = null; |
| this._target = targetId ? WI.targetManager.targetForIdentifier(targetId) : WI.mainTarget; |
| |
| // Exact sizes if loaded over the network or cache. |
| this._requestHeadersTransferSize = NaN; |
| this._requestBodyTransferSize = NaN; |
| this._responseHeadersTransferSize = NaN; |
| this._responseBodyTransferSize = NaN; |
| this._responseBodySize = NaN; |
| this._cachedResponseBodySize = NaN; |
| |
| // Estimated sizes (if backend does not provide metrics). |
| this._estimatedSize = NaN; |
| this._estimatedTransferSize = NaN; |
| this._estimatedResponseHeadersSize = NaN; |
| |
| if (this._initiatorSourceCodeLocation && this._initiatorSourceCodeLocation.sourceCode instanceof WI.Resource) |
| this._initiatorSourceCodeLocation.sourceCode.addInitiatedResource(this); |
| } |
| |
| // Static |
| |
| static resolvedType(type, mimeType) |
| { |
| if (type && type !== WI.Resource.Type.Other) |
| return type; |
| |
| return Resource.typeFromMIMEType(mimeType); |
| } |
| |
| static typeFromMIMEType(mimeType) |
| { |
| if (!mimeType) |
| return WI.Resource.Type.Other; |
| |
| mimeType = parseMIMEType(mimeType).type; |
| |
| if (mimeType in WI.Resource._mimeTypeMap) |
| return WI.Resource._mimeTypeMap[mimeType]; |
| |
| if (mimeType.startsWith("image/")) |
| return WI.Resource.Type.Image; |
| |
| if (mimeType.startsWith("font/")) |
| return WI.Resource.Type.Font; |
| |
| return WI.Resource.Type.Other; |
| } |
| |
| static displayNameForType(type, plural) |
| { |
| switch (type) { |
| case WI.Resource.Type.Document: |
| if (plural) |
| return WI.UIString("Documents"); |
| return WI.UIString("Document"); |
| case WI.Resource.Type.Stylesheet: |
| if (plural) |
| return WI.UIString("Stylesheets"); |
| return WI.UIString("Stylesheet"); |
| case WI.Resource.Type.Image: |
| if (plural) |
| return WI.UIString("Images"); |
| return WI.UIString("Image"); |
| case WI.Resource.Type.Font: |
| if (plural) |
| return WI.UIString("Fonts"); |
| return WI.UIString("Font"); |
| case WI.Resource.Type.Script: |
| if (plural) |
| return WI.UIString("Scripts"); |
| return WI.UIString("Script"); |
| case WI.Resource.Type.XHR: |
| if (plural) |
| return WI.UIString("XHRs"); |
| return WI.UIString("XHR"); |
| case WI.Resource.Type.Fetch: |
| if (plural) |
| return WI.UIString("Fetches"); |
| return WI.UIString("Fetch"); |
| case WI.Resource.Type.Ping: |
| if (plural) |
| return WI.UIString("Pings"); |
| return WI.UIString("Ping"); |
| case WI.Resource.Type.Beacon: |
| if (plural) |
| return WI.UIString("Beacons"); |
| return WI.UIString("Beacon"); |
| case WI.Resource.Type.WebSocket: |
| if (plural) |
| return WI.UIString("Sockets"); |
| return WI.UIString("Socket"); |
| case WI.Resource.Type.Other: |
| return WI.UIString("Other"); |
| default: |
| console.error("Unknown resource type", type); |
| return null; |
| } |
| } |
| |
| static displayNameForProtocol(protocol) |
| { |
| switch (protocol) { |
| case "h2": |
| return "HTTP/2"; |
| case "http/1.0": |
| return "HTTP/1.0"; |
| case "http/1.1": |
| return "HTTP/1.1"; |
| case "spdy/2": |
| return "SPDY/2"; |
| case "spdy/3": |
| return "SPDY/3"; |
| case "spdy/3.1": |
| return "SPDY/3.1"; |
| default: |
| return null; |
| } |
| } |
| |
| static comparePriority(a, b) |
| { |
| console.assert(typeof a === "symbol"); |
| console.assert(typeof b === "symbol"); |
| |
| const map = { |
| [WI.Resource.NetworkPriority.Unknown]: 0, |
| [WI.Resource.NetworkPriority.Low]: 1, |
| [WI.Resource.NetworkPriority.Medium]: 2, |
| [WI.Resource.NetworkPriority.High]: 3, |
| }; |
| |
| let aNum = map[a] || 0; |
| let bNum = map[b] || 0; |
| return aNum - bNum; |
| } |
| |
| static displayNameForPriority(priority) |
| { |
| switch (priority) { |
| case WI.Resource.NetworkPriority.Low: |
| return WI.UIString("Low"); |
| case WI.Resource.NetworkPriority.Medium: |
| return WI.UIString("Medium"); |
| case WI.Resource.NetworkPriority.High: |
| return WI.UIString("High"); |
| default: |
| return null; |
| } |
| } |
| |
| static responseSourceFromPayload(source) |
| { |
| if (!source) |
| return WI.Resource.ResponseSource.Unknown; |
| |
| switch (source) { |
| case NetworkAgent.ResponseSource.Unknown: |
| return WI.Resource.ResponseSource.Unknown; |
| case NetworkAgent.ResponseSource.Network: |
| return WI.Resource.ResponseSource.Network; |
| case NetworkAgent.ResponseSource.MemoryCache: |
| return WI.Resource.ResponseSource.MemoryCache; |
| case NetworkAgent.ResponseSource.DiskCache: |
| return WI.Resource.ResponseSource.DiskCache; |
| case NetworkAgent.ResponseSource.ServiceWorker: |
| return WI.Resource.ResponseSource.ServiceWorker; |
| default: |
| console.error("Unknown response source type", source); |
| return WI.Resource.ResponseSource.Unknown; |
| } |
| } |
| |
| static networkPriorityFromPayload(priority) |
| { |
| switch (priority) { |
| case NetworkAgent.MetricsPriority.Low: |
| return WI.Resource.NetworkPriority.Low; |
| case NetworkAgent.MetricsPriority.Medium: |
| return WI.Resource.NetworkPriority.Medium; |
| case NetworkAgent.MetricsPriority.High: |
| return WI.Resource.NetworkPriority.High; |
| default: |
| console.error("Unknown metrics priority", priority); |
| return WI.Resource.NetworkPriority.Unknown; |
| } |
| } |
| |
| static connectionIdentifierFromPayload(connectionIdentifier) |
| { |
| // Map backend connection identifiers to an easier to read number. |
| if (!WI.Resource.connectionIdentifierMap) { |
| WI.Resource.connectionIdentifierMap = new Map; |
| WI.Resource.nextConnectionIdentifier = 1; |
| } |
| |
| let id = WI.Resource.connectionIdentifierMap.get(connectionIdentifier); |
| if (id) |
| return id; |
| |
| id = WI.Resource.nextConnectionIdentifier++; |
| WI.Resource.connectionIdentifierMap.set(connectionIdentifier, id); |
| return id; |
| } |
| |
| // Public |
| |
| get target() { return this._target; } |
| get type() { return this._type; } |
| get loaderIdentifier() { return this._loaderIdentifier; } |
| get requestIdentifier() { return this._requestIdentifier; } |
| get requestMethod() { return this._requestMethod; } |
| get requestData() { return this._requestData; } |
| get statusCode() { return this._statusCode; } |
| get statusText() { return this._statusText; } |
| get responseSource() { return this._responseSource; } |
| get timingData() { return this._timingData; } |
| get protocol() { return this._protocol; } |
| get priority() { return this._priority; } |
| get remoteAddress() { return this._remoteAddress; } |
| get connectionIdentifier() { return this._connectionIdentifier; } |
| |
| get url() |
| { |
| return this._url; |
| } |
| |
| get urlComponents() |
| { |
| if (!this._urlComponents) |
| this._urlComponents = parseURL(this._url); |
| return this._urlComponents; |
| } |
| |
| get displayName() |
| { |
| return WI.displayNameForURL(this._url, this.urlComponents); |
| } |
| |
| get displayURL() |
| { |
| const isMultiLine = true; |
| const dataURIMaxSize = 64; |
| return WI.truncateURL(this._url, isMultiLine, dataURIMaxSize); |
| } |
| |
| get initiatorSourceCodeLocation() |
| { |
| return this._initiatorSourceCodeLocation; |
| } |
| |
| get initiatedResources() |
| { |
| return this._initiatedResources; |
| } |
| |
| get originalRequestWillBeSentTimestamp() |
| { |
| return this._originalRequestWillBeSentTimestamp; |
| } |
| |
| 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 WI.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 === WI.Resource.typeFromMIMEType(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 WI.Resource.Type.Stylesheet: |
| return "text/css"; |
| case WI.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; |
| } |
| |
| createObjectURL() |
| { |
| // If content is not available, fallback to using original URL. |
| // The client may try to revoke it, but nothing will happen. |
| let content = this.content; |
| if (!content) |
| return this._url; |
| |
| if (content instanceof Blob) |
| return URL.createObjectURL(content); |
| |
| if (typeof content === "string") { |
| let blob = textToBlob(content, this._mimeType); |
| return URL.createObjectURL(blob); |
| } |
| |
| return null; |
| } |
| |
| isMainResource() |
| { |
| return this._parentFrame ? this._parentFrame.mainResource === this : false; |
| } |
| |
| addInitiatedResource(resource) |
| { |
| if (!(resource instanceof WI.Resource)) |
| return; |
| |
| this._initiatedResources.push(resource); |
| |
| this.dispatchEventToListeners(WI.Resource.Event.InitiatedResourcesDidChange); |
| } |
| |
| get parentFrame() |
| { |
| return this._parentFrame; |
| } |
| |
| get finished() |
| { |
| return this._finished; |
| } |
| |
| get failed() |
| { |
| return this._failed; |
| } |
| |
| get canceled() |
| { |
| return this._canceled; |
| } |
| |
| get failureReasonText() |
| { |
| return this._failureReasonText; |
| } |
| |
| get queryStringParameters() |
| { |
| if (this._queryStringParameters === undefined) |
| this._queryStringParameters = parseQueryString(this.urlComponents.queryString, true); |
| return this._queryStringParameters; |
| } |
| |
| get requestFormParameters() |
| { |
| if (this._requestFormParameters === undefined) |
| this._requestFormParameters = this.hasRequestFormParameters() ? parseQueryString(this.requestData, true) : null; |
| return this._requestFormParameters; |
| } |
| |
| get requestDataContentType() |
| { |
| return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null; |
| } |
| |
| get requestHeaders() |
| { |
| return this._requestHeaders; |
| } |
| |
| get responseHeaders() |
| { |
| return this._responseHeaders; |
| } |
| |
| get requestCookies() |
| { |
| if (!this._requestCookies) |
| this._requestCookies = WI.Cookie.parseCookieRequestHeader(this._requestHeaders.valueForCaseInsensitiveKey("Cookie")); |
| |
| return this._requestCookies; |
| } |
| |
| get responseCookies() |
| { |
| if (!this._responseCookies) { |
| // FIXME: The backend sends multiple "Set-Cookie" headers in one "Set-Cookie" with multiple values |
| // separated by ", ". This doesn't allow us to safely distinguish between a ", " that separates |
| // multiple headers or one that may be valid part of a Cookie's value or attribute, such as the |
| // ", " in the the date format "Expires=Tue, 03-Oct-2017 04:39:21 GMT". To improve heuristics |
| // we do a negative lookahead for numbers, but we can still fail on cookie values containing ", ". |
| let rawCombinedHeader = this._responseHeaders.valueForCaseInsensitiveKey("Set-Cookie") || ""; |
| let setCookieHeaders = rawCombinedHeader.split(/, (?![0-9])/); |
| let cookies = []; |
| for (let header of setCookieHeaders) { |
| let cookie = WI.Cookie.parseSetCookieResponseHeader(header); |
| if (cookie) |
| cookies.push(cookie); |
| } |
| this._responseCookies = cookies; |
| } |
| |
| return this._responseCookies; |
| } |
| |
| get requestSentTimestamp() |
| { |
| return this._requestSentTimestamp; |
| } |
| |
| get requestSentWalltime() |
| { |
| return this._requestSentWalltime; |
| } |
| |
| get requestSentDate() |
| { |
| return isNaN(this._requestSentWalltime) ? null : new Date(this._requestSentWalltime * 1000); |
| } |
| |
| get lastRedirectReceivedTimestamp() |
| { |
| return this._lastRedirectReceivedTimestamp; |
| } |
| |
| get responseReceivedTimestamp() |
| { |
| return this._responseReceivedTimestamp; |
| } |
| |
| get lastDataReceivedTimestamp() |
| { |
| return this._lastDataReceivedTimestamp; |
| } |
| |
| get finishedOrFailedTimestamp() |
| { |
| return this._finishedOrFailedTimestamp; |
| } |
| |
| get firstTimestamp() |
| { |
| return this.timingData.startTime || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp; |
| } |
| |
| get lastTimestamp() |
| { |
| return this.timingData.responseEnd || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp; |
| } |
| |
| get latency() |
| { |
| return this.timingData.responseStart - this.timingData.requestStart; |
| } |
| |
| get receiveDuration() |
| { |
| return this.timingData.responseEnd - this.timingData.responseStart; |
| } |
| |
| get totalDuration() |
| { |
| return this.timingData.responseEnd - this.timingData.startTime; |
| } |
| |
| get cached() |
| { |
| return this._cached; |
| } |
| |
| get requestHeadersTransferSize() { return this._requestHeadersTransferSize; } |
| get requestBodyTransferSize() { return this._requestBodyTransferSize; } |
| get responseHeadersTransferSize() { return this._responseHeadersTransferSize; } |
| get responseBodyTransferSize() { return this._responseBodyTransferSize; } |
| get cachedResponseBodySize() { return this._cachedResponseBodySize; } |
| |
| get size() |
| { |
| if (!isNaN(this._cachedResponseBodySize)) |
| return this._cachedResponseBodySize; |
| |
| if (!isNaN(this._responseBodySize) && this._responseBodySize !== 0) |
| return this._responseBodySize; |
| |
| return this._estimatedSize; |
| } |
| |
| get networkEncodedSize() |
| { |
| return this._responseBodyTransferSize; |
| } |
| |
| get networkDecodedSize() |
| { |
| return this._responseBodySize; |
| } |
| |
| get networkTotalTransferSize() |
| { |
| return this._responseHeadersTransferSize + this._responseBodyTransferSize; |
| } |
| |
| get estimatedNetworkEncodedSize() |
| { |
| let exact = this.networkEncodedSize; |
| if (!isNaN(exact)) |
| return exact; |
| |
| if (this._cached) |
| return 0; |
| |
| // FIXME: <https://webkit.org/b/158463> Network: Correctly report encoded data length (transfer size) from CFNetwork to NetworkResourceLoader |
| // macOS provides the decoded transfer size instead of the encoded size |
| // for estimatedTransferSize. So prefer the "Content-Length" property |
| // on mac if it is available. |
| if (WI.Platform.name === "mac") { |
| let contentLength = Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length")); |
| if (!isNaN(contentLength)) |
| return contentLength; |
| } |
| |
| if (!isNaN(this._estimatedTransferSize)) |
| return this._estimatedTransferSize; |
| |
| // 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._estimatedSize); |
| } |
| |
| get estimatedTotalTransferSize() |
| { |
| let exact = this.networkTotalTransferSize; |
| if (!isNaN(exact)) |
| return exact; |
| |
| if (this.statusCode === 304) // Not modified |
| return this._estimatedResponseHeadersSize; |
| |
| if (this._cached) |
| return 0; |
| |
| return this._estimatedResponseHeadersSize + this.estimatedNetworkEncodedSize; |
| } |
| |
| get compressed() |
| { |
| let contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding"); |
| return !!(contentEncoding && /\b(?:gzip|deflate)\b/.test(contentEncoding)); |
| } |
| |
| get scripts() |
| { |
| return this._scripts || []; |
| } |
| |
| scriptForLocation(sourceCodeLocation) |
| { |
| console.assert(!(this instanceof WI.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(url, requestHeaders, elapsedTime) |
| { |
| console.assert(!this._finished); |
| console.assert(!this._failed); |
| console.assert(!this._canceled); |
| |
| var oldURL = this._url; |
| |
| if (url) |
| this._url = url; |
| |
| this._requestHeaders = requestHeaders || {}; |
| this._requestCookies = null; |
| this._lastRedirectReceivedTimestamp = elapsedTime || NaN; |
| |
| if (oldURL !== url) { |
| // Delete the URL components so the URL is re-parsed the next time it is requested. |
| this._urlComponents = null; |
| |
| this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL}); |
| } |
| |
| this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange); |
| this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange); |
| } |
| |
| hasResponse() |
| { |
| return !isNaN(this._statusCode) || this._finished || this._failed; |
| } |
| |
| hasRequestFormParameters() |
| { |
| let requestDataContentType = this.requestDataContentType; |
| return requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i); |
| } |
| |
| updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, elapsedTime, timingData, source) |
| { |
| console.assert(!this._finished); |
| console.assert(!this._failed); |
| console.assert(!this._canceled); |
| |
| let oldURL = this._url; |
| let oldMIMEType = this._mimeType; |
| let oldType = this._type; |
| |
| if (type in WI.Resource.Type) |
| type = WI.Resource.Type[type]; |
| |
| if (url) |
| this._url = url; |
| |
| this._mimeType = mimeType; |
| this._type = Resource.resolvedType(type, mimeType); |
| this._statusCode = statusCode; |
| this._statusText = statusText; |
| this._responseHeaders = responseHeaders || {}; |
| this._responseCookies = null; |
| this._responseReceivedTimestamp = elapsedTime || NaN; |
| this._timingData = WI.ResourceTimingData.fromPayload(timingData, this); |
| |
| if (source) |
| this._responseSource = WI.Resource.responseSourceFromPayload(source); |
| |
| const headerBaseSize = 12; // Length of "HTTP/1.1 ", " ", and "\r\n". |
| const headerPad = 4; // Length of ": " and "\r\n". |
| this._estimatedResponseHeadersSize = String(this._statusCode).length + this._statusText.length + headerBaseSize; |
| for (let name in this._responseHeaders) |
| this._estimatedResponseHeadersSize += name.length + this._responseHeaders[name].length + headerPad; |
| |
| if (!this._cached) { |
| if (statusCode === 304 || (this._responseSource === WI.Resource.ResponseSource.MemoryCache || this._responseSource === WI.Resource.ResponseSource.DiskCache)) |
| this.markAsCached(); |
| } |
| |
| if (oldURL !== url) { |
| // Delete the URL components so the URL is re-parsed the next time it is requested. |
| this._urlComponents = null; |
| |
| this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL}); |
| } |
| |
| if (oldMIMEType !== mimeType) { |
| // Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested. |
| this._mimeTypeComponents = null; |
| |
| this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType}); |
| } |
| |
| if (oldType !== type) |
| this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType}); |
| |
| console.assert(isNaN(this._estimatedSize)); |
| console.assert(isNaN(this._estimatedTransferSize)); |
| |
| // 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(WI.Resource.Event.TransferSizeDidChange); |
| |
| this.dispatchEventToListeners(WI.Resource.Event.ResponseReceived); |
| this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange); |
| } |
| |
| updateWithMetrics(metrics) |
| { |
| this._receivedNetworkLoadMetrics = true; |
| |
| if (metrics.protocol) |
| this._protocol = metrics.protocol; |
| if (metrics.priority) |
| this._priority = WI.Resource.networkPriorityFromPayload(metrics.priority); |
| if (metrics.remoteAddress) |
| this._remoteAddress = metrics.remoteAddress; |
| if (metrics.connectionIdentifier) |
| this._connectionIdentifier = WI.Resource.connectionIdentifierFromPayload(metrics.connectionIdentifier); |
| if (metrics.requestHeaders) { |
| this._requestHeaders = metrics.requestHeaders; |
| this._requestCookies = null; |
| this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange); |
| } |
| |
| if ("requestHeaderBytesSent" in metrics) { |
| this._requestHeadersTransferSize = metrics.requestHeaderBytesSent; |
| this._requestBodyTransferSize = metrics.requestBodyBytesSent; |
| this._responseHeadersTransferSize = metrics.responseHeaderBytesReceived; |
| this._responseBodyTransferSize = metrics.responseBodyBytesReceived; |
| this._responseBodySize = metrics.responseBodyDecodedSize; |
| |
| console.assert(this._requestHeadersTransferSize >= 0); |
| console.assert(this._requestBodyTransferSize >= 0); |
| console.assert(this._responseHeadersTransferSize >= 0); |
| console.assert(this._responseBodyTransferSize >= 0); |
| console.assert(this._responseBodySize >= 0); |
| |
| this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize: this._estimatedSize}); |
| this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange); |
| } |
| |
| this.dispatchEventToListeners(WI.Resource.Event.MetricsDidChange); |
| } |
| |
| setCachedResponseBodySize(size) |
| { |
| console.assert(!isNaN(size), "Size should be a valid number."); |
| console.assert(isNaN(this._cachedResponseBodySize), "This should only be set once."); |
| console.assert(this._estimatedSize === size, "The legacy path was updated already and matches."); |
| |
| this._cachedResponseBodySize = size; |
| } |
| |
| requestContentFromBackend() |
| { |
| // 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) |
| return NetworkAgent.getResponseBody(this._requestIdentifier); |
| |
| // There is no request identifier or frame to request content from. |
| if (this._parentFrame) |
| return PageAgent.getResourceContent(this._parentFrame.id, this._url); |
| |
| return Promise.reject(new Error("Content request failed.")); |
| } |
| |
| increaseSize(dataLength, elapsedTime) |
| { |
| console.assert(dataLength >= 0); |
| console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated size."); |
| |
| if (isNaN(this._estimatedSize)) |
| this._estimatedSize = 0; |
| |
| let previousSize = this._estimatedSize; |
| |
| this._estimatedSize += dataLength; |
| |
| this._lastDataReceivedTimestamp = elapsedTime || NaN; |
| |
| this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize}); |
| |
| // The estimatedTransferSize is based off of size when status is not 304 or Content-Length is missing. |
| if (isNaN(this._estimatedTransferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length")) |
| this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange); |
| } |
| |
| increaseTransferSize(encodedDataLength) |
| { |
| console.assert(encodedDataLength >= 0); |
| console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated transfer size."); |
| |
| if (isNaN(this._estimatedTransferSize)) |
| this._estimatedTransferSize = 0; |
| this._estimatedTransferSize += encodedDataLength; |
| |
| this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange); |
| } |
| |
| markAsCached() |
| { |
| this._cached = true; |
| |
| this.dispatchEventToListeners(WI.Resource.Event.CacheStatusDidChange); |
| |
| // The transferSize starts returning 0 when cached is true, unless status is 304. |
| if (this._statusCode !== 304) |
| this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange); |
| } |
| |
| markAsFinished(elapsedTime) |
| { |
| console.assert(!this._failed); |
| console.assert(!this._canceled); |
| |
| this._finished = true; |
| this._finishedOrFailedTimestamp = elapsedTime || NaN; |
| this._timingData.markResponseEndTime(elapsedTime || NaN); |
| |
| if (this._finishThenRequestContentPromise) |
| this._finishThenRequestContentPromise = null; |
| |
| this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFinish); |
| this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange); |
| } |
| |
| markAsFailed(canceled, elapsedTime, errorText) |
| { |
| console.assert(!this._finished); |
| |
| this._failed = true; |
| this._canceled = canceled; |
| this._finishedOrFailedTimestamp = elapsedTime || NaN; |
| |
| if (!this._failureReasonText) |
| this._failureReasonText = errorText || null; |
| |
| this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFail); |
| this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange); |
| } |
| |
| revertMarkAsFinished() |
| { |
| console.assert(!this._failed); |
| console.assert(!this._canceled); |
| console.assert(this._finished); |
| |
| this._finished = false; |
| this._finishedOrFailedTimestamp = NaN; |
| } |
| |
| legacyMarkServedFromMemoryCache() |
| { |
| // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the MemoryCache. |
| console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown); |
| |
| this._responseSource = WI.Resource.ResponseSource.MemoryCache; |
| |
| this.markAsCached(); |
| } |
| |
| legacyMarkServedFromDiskCache() |
| { |
| // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the DiskCache. |
| console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown); |
| |
| this._responseSource = WI.Resource.ResponseSource.DiskCache; |
| |
| this.markAsCached(); |
| } |
| |
| isLoading() |
| { |
| return !this._finished && !this._failed; |
| } |
| |
| hadLoadingError() |
| { |
| return this._failed || this._canceled || this._statusCode >= 400; |
| } |
| |
| getImageSize(callback) |
| { |
| // Throw an error in the case this resource is not an image. |
| if (this.type !== WI.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 !== undefined) { |
| callback(this._imageSize); |
| return; |
| } |
| |
| var objectURL = null; |
| |
| // Event handler for the image "load" event. |
| function imageDidLoad() { |
| URL.revokeObjectURL(objectURL); |
| |
| // Cache the image metrics. |
| this._imageSize = { |
| width: image.width, |
| height: image.height |
| }; |
| |
| callback(this._imageSize); |
| } |
| |
| function requestContentFailure() { |
| this._imageSize = null; |
| callback(this._imageSize); |
| } |
| |
| // Create an <img> 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 using an object URL once we've obtained its data. |
| this.requestContent().then((content) => { |
| objectURL = image.src = content.sourceCode.createObjectURL(); |
| if (!objectURL) |
| requestContentFailure.call(this); |
| }, requestContentFailure.bind(this)); |
| } |
| |
| requestContent() |
| { |
| if (this._finished) |
| return super.requestContent(); |
| |
| if (this._failed) |
| return Promise.resolve({error: WI.UIString("An error occurred trying to load the resource.")}); |
| |
| if (!this._finishThenRequestContentPromise) { |
| this._finishThenRequestContentPromise = new Promise((resolve, reject) => { |
| this.addEventListener(WI.Resource.Event.LoadingDidFinish, resolve); |
| this.addEventListener(WI.Resource.Event.LoadingDidFail, reject); |
| }).then(WI.SourceCode.prototype.requestContent.bind(this)); |
| } |
| |
| return this._finishThenRequestContentPromise; |
| } |
| |
| associateWithScript(script) |
| { |
| if (!this._scripts) |
| this._scripts = []; |
| |
| this._scripts.push(script); |
| |
| if (this._type === WI.Resource.Type.Other || this._type === WI.Resource.Type.XHR) { |
| let oldType = this._type; |
| this._type = WI.Resource.Type.Script; |
| this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType}); |
| } |
| } |
| |
| saveIdentityToCookie(cookie) |
| { |
| cookie[WI.Resource.URLCookieKey] = this.url.hash; |
| cookie[WI.Resource.MainResourceCookieKey] = this.isMainResource(); |
| } |
| |
| generateCURLCommand() |
| { |
| function escapeStringPosix(str) { |
| function escapeCharacter(x) { |
| let code = x.charCodeAt(0); |
| let hex = code.toString(16); |
| if (code < 256) |
| return "\\x" + hex.padStart(2, "0"); |
| return "\\u" + hex.padStart(4, "0"); |
| } |
| |
| if (/[^\x20-\x7E]|'/.test(str)) { |
| // Use ANSI-C quoting syntax. |
| return "$'" + str.replace(/\\/g, "\\\\") |
| .replace(/'/g, "\\'") |
| .replace(/\n/g, "\\n") |
| .replace(/\r/g, "\\r") |
| .replace(/!/g, "\\041") |
| .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'"; |
| } else { |
| // Use single quote syntax. |
| return `'${str}'`; |
| } |
| } |
| |
| let command = ["curl " + escapeStringPosix(this.url).replace(/[[{}\]]/g, "\\$&")]; |
| command.push(`-X${this.requestMethod}`); |
| |
| for (let key in this.requestHeaders) |
| command.push("-H " + escapeStringPosix(`${key}: ${this.requestHeaders[key]}`)); |
| |
| if (this.requestDataContentType && this.requestMethod !== "GET" && this.requestData) { |
| if (this.requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) |
| command.push("--data " + escapeStringPosix(this.requestData)); |
| else |
| command.push("--data-binary " + escapeStringPosix(this.requestData)); |
| } |
| |
| let curlCommand = command.join(" \\\n"); |
| InspectorFrontendHost.copyText(curlCommand); |
| return curlCommand; |
| } |
| }; |
| |
| WI.Resource.TypeIdentifier = "resource"; |
| WI.Resource.URLCookieKey = "resource-url"; |
| WI.Resource.MainResourceCookieKey = "resource-is-main-resource"; |
| |
| WI.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", |
| MetricsDidChange: "resource-metrics-did-change", |
| InitiatedResourcesDidChange: "resource-initiated-resources-did-change", |
| }; |
| |
| // Keep these in sync with the "ResourceType" enum defined by the "Page" domain. |
| WI.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", |
| Fetch: "resource-type-fetch", |
| Ping: "resource-type-ping", |
| Beacon: "resource-type-beacon", |
| WebSocket: "resource-type-websocket", |
| Other: "resource-type-other", |
| }; |
| |
| WI.Resource.ResponseSource = { |
| Unknown: Symbol("unknown"), |
| Network: Symbol("network"), |
| MemoryCache: Symbol("memory-cache"), |
| DiskCache: Symbol("disk-cache"), |
| ServiceWorker: Symbol("service-worker"), |
| }; |
| |
| WI.Resource.NetworkPriority = { |
| Unknown: Symbol("unknown"), |
| Low: Symbol("low"), |
| Medium: Symbol("medium"), |
| High: Symbol("high"), |
| }; |
| |
| // This MIME Type map is private, use WI.Resource.typeFromMIMEType(). |
| WI.Resource._mimeTypeMap = { |
| "text/html": WI.Resource.Type.Document, |
| "text/xml": WI.Resource.Type.Document, |
| "text/plain": WI.Resource.Type.Document, |
| "application/xhtml+xml": WI.Resource.Type.Document, |
| |
| "text/css": WI.Resource.Type.Stylesheet, |
| "text/xsl": WI.Resource.Type.Stylesheet, |
| "text/x-less": WI.Resource.Type.Stylesheet, |
| "text/x-sass": WI.Resource.Type.Stylesheet, |
| "text/x-scss": WI.Resource.Type.Stylesheet, |
| |
| "application/pdf": WI.Resource.Type.Image, |
| "image/svg+xml": WI.Resource.Type.Image, |
| |
| "application/x-font-type1": WI.Resource.Type.Font, |
| "application/x-font-ttf": WI.Resource.Type.Font, |
| "application/x-font-woff": WI.Resource.Type.Font, |
| "application/x-truetype-font": WI.Resource.Type.Font, |
| |
| "text/javascript": WI.Resource.Type.Script, |
| "text/ecmascript": WI.Resource.Type.Script, |
| "application/javascript": WI.Resource.Type.Script, |
| "application/ecmascript": WI.Resource.Type.Script, |
| "application/x-javascript": WI.Resource.Type.Script, |
| "application/json": WI.Resource.Type.Script, |
| "application/x-json": WI.Resource.Type.Script, |
| "text/x-javascript": WI.Resource.Type.Script, |
| "text/x-json": WI.Resource.Type.Script, |
| "text/javascript1.1": WI.Resource.Type.Script, |
| "text/javascript1.2": WI.Resource.Type.Script, |
| "text/javascript1.3": WI.Resource.Type.Script, |
| "text/jscript": WI.Resource.Type.Script, |
| "text/livescript": WI.Resource.Type.Script, |
| "text/x-livescript": WI.Resource.Type.Script, |
| "text/typescript": WI.Resource.Type.Script, |
| "text/typescript-jsx": WI.Resource.Type.Script, |
| "text/jsx": WI.Resource.Type.Script, |
| "text/x-clojure": WI.Resource.Type.Script, |
| "text/x-coffeescript": WI.Resource.Type.Script, |
| }; |