| /* |
| * Copyright (C) 2017-2019 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 NOEVENT 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. |
| */ |
| |
| // LocalResource is a complete resource constructed by the frontend. It |
| // does not need to be tied to an object in the backend. The local resource |
| // does not need to be complete at creation and can get its content later. |
| // |
| // Construction values try to mimic protocol inputs to WI.Resource: |
| // |
| // request: { url, method, headers, timestamp, walltime, finishedTimestamp data } |
| // response: { mimeType, headers, statusCode, statusText, failureReasonText, content, base64Encoded } |
| // metrics: { responseSource, protocol, priority, remoteAddress, connectionIdentifier, sizes } |
| // timing: { startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd } |
| // isLocalResourceOverride: <boolean> |
| |
| WI.LocalResource = class LocalResource extends WI.Resource |
| { |
| constructor({request, response, metrics, timing, isLocalResourceOverride}) |
| { |
| console.assert(request); |
| console.assert(typeof request.url === "string"); |
| console.assert(response); |
| |
| metrics = metrics || {}; |
| timing = timing || {}; |
| |
| super(request.url, { |
| mimeType: response.mimeType || (response.headers || {}).valueForCaseInsensitiveKey("Content-Type") || null, |
| requestMethod: request.method, |
| requestHeaders: request.headers, |
| requestData: request.data, |
| requestSentTimestamp: request.timestamp, |
| requestSentWalltime: request.walltime, |
| }); |
| |
| // NOTE: This directly overwrites WI.Resource properties. |
| this._finishedOrFailedTimestamp = request.finishedTimestamp || NaN; |
| this._statusCode = response.statusCode || NaN; |
| this._statusText = response.statusText || null; |
| this._responseHeaders = response.headers || {}; |
| this._failureReasonText = response.failureReasonText; |
| this._timingData = new WI.ResourceTimingData(this, timing); |
| |
| this._responseSource = metrics.responseSource || WI.Resource.ResponseSource.Unknown; |
| this._protocol = metrics.protocol || null; |
| this._priority = metrics.priority || WI.Resource.NetworkPriority.Unknown; |
| this._remoteAddress = metrics.remoteAddress || null; |
| this._connectionIdentifier = metrics.connectionIdentifier || null; |
| this._requestHeadersTransferSize = !isNaN(metrics.requestHeaderBytesSent) ? metrics.requestHeaderBytesSent : NaN; |
| this._requestBodyTransferSize = !isNaN(metrics.requestBodyBytesSent) ? metrics.requestBodyBytesSent : NaN; |
| this._responseHeadersTransferSize = !isNaN(metrics.responseHeaderBytesReceived) ? metrics.responseHeaderBytesReceived : NaN; |
| this._responseBodyTransferSize = !isNaN(metrics.responseBodyBytesReceived) ? metrics.responseBodyBytesReceived : NaN; |
| this._responseBodySize = !isNaN(metrics.responseBodyDecodedSize) ? metrics.responseBodyDecodedSize : NaN; |
| |
| // Access to the content. |
| this._localContent = response.content; |
| this._localContentIsBase64Encoded = response.base64Encoded; |
| |
| // LocalResource specific. |
| this._isLocalResourceOverride = isLocalResourceOverride || false; |
| |
| // Finalize WI.Resource. |
| this._finished = true; |
| this._failed = false; // FIXME: How should we denote a failure? Assume from status code / failure reason? |
| this._cached = false; // FIXME: How should we denote cached? Assume from response source? |
| |
| // Finalize WI.SourceCode. |
| this._originalRevision = new WI.SourceCodeRevision(this, this._localContent); |
| this._currentRevision = this._originalRevision; |
| } |
| |
| // Static |
| |
| static headersArrayToHeadersObject(headers) |
| { |
| let result = {}; |
| if (headers) { |
| for (let {name, value} of headers) |
| result[name] = value; |
| } |
| return result; |
| } |
| |
| static fromHAREntry(entry, archiveStartWalltime) |
| { |
| // FIXME: <https://webkit.org/b/195694> Web Inspector: HAR Extension for Redirect Timing Info |
| |
| let {request, response, startedDateTime, timings} = entry; |
| let requestSentWalltime = WI.HARBuilder.dateFromHARDate(startedDateTime) / 1000; |
| let requestSentTimestamp = requestSentWalltime - archiveStartWalltime; |
| let finishedTimestamp = NaN; |
| |
| let timing = { |
| startTime: NaN, |
| domainLookupStart: NaN, |
| domainLookupEnd: NaN, |
| connectStart: NaN, |
| connectEnd: NaN, |
| secureConnectionStart: NaN, |
| requestStart: NaN, |
| responseStart: NaN, |
| responseEnd: NaN, |
| }; |
| |
| if (!isNaN(requestSentWalltime) && !isNaN(archiveStartWalltime)) { |
| let hasBlocked = timings.blocked !== -1; |
| let hasDNS = timings.dns !== -1; |
| let hasConnect = timings.connect !== -1; |
| let hasSecureConnect = timings.ssl !== -1; |
| |
| // FIXME: <https://webkit.org/b/195692> Web Inspector: ResourceTimingData should allow a startTime of 0 |
| timing.startTime = requestSentTimestamp || Number.EPSILON; |
| timing.fetchStart = timing.startTime; |
| |
| let accumulation = timing.startTime; |
| |
| if (hasBlocked) |
| accumulation += (timings.blocked / 1000); |
| |
| if (hasDNS) { |
| timing.domainLookupStart = accumulation; |
| accumulation += (timings.dns / 1000); |
| timing.domainLookupEnd = accumulation; |
| } |
| |
| if (hasConnect) { |
| timing.connectStart = accumulation; |
| accumulation += (timings.connect / 1000); |
| timing.connectEnd = accumulation; |
| |
| if (hasSecureConnect) |
| timing.secureConnectionStart = timing.connectEnd - (timings.ssl / 1000); |
| } |
| |
| accumulation += (timings.send / 1000); |
| timing.requestStart = accumulation; |
| |
| accumulation += (timings.wait / 1000); |
| timing.responseStart = accumulation; |
| |
| accumulation += (timings.receive / 1000); |
| timing.responseEnd = accumulation; |
| |
| finishedTimestamp = timing.responseEnd; |
| } |
| |
| let serverAddress = entry.serverIPAddress || null; |
| if (serverAddress && typeof entry._serverPort === "number") |
| serverAddress += ":" + entry._serverPort; |
| |
| return new WI.LocalResource({ |
| request: { |
| url: request.url, |
| method: request.method, |
| headers: LocalResource.headersArrayToHeadersObject(request.headers), |
| timestamp: requestSentTimestamp, |
| walltime: requestSentWalltime, |
| finishedTimestamp: finishedTimestamp, |
| data: request.postData ? request.postData.text : null, |
| }, |
| response: { |
| headers: LocalResource.headersArrayToHeadersObject(response.headers), |
| mimeType: response.content.mimeType, |
| statusCode: response.status, |
| statusText: response.statusText, |
| failureReasonText: response._error || null, |
| content: response.content.text, |
| base64Encoded: response.content.encoding === "base64", |
| }, |
| metrics: { |
| responseSource: WI.HARBuilder.responseSourceFromHARFetchType(entry._fetchType), |
| protocol: WI.HARBuilder.protocolFromHARProtocol(response.httpVersion), |
| priority: WI.HARBuilder.networkPriorityFromHARPriority(entry._priority), |
| remoteAddress: serverAddress, |
| connectionIdentifier: entry.connection ? parseInt(entry.connection) : null, |
| requestHeaderBytesSent: request.headersSize >= 0 ? request.headersSize : NaN, |
| requestBodyBytesSent: request.bodySize >= 0 ? request.bodySize : NaN, |
| responseHeaderBytesReceived: response.headersSize >= 0 ? response.headersSize : NaN, |
| responseBodyBytesReceived: response.bodySize >= 0 ? response.bodySize : NaN, |
| responseBodyDecodedSize: response.content.size || NaN, |
| }, |
| timing, |
| }); |
| } |
| |
| // Import / Export |
| |
| static fromJSON(json) |
| { |
| return new WI.LocalResource(json); |
| } |
| |
| toJSON(key) |
| { |
| return { |
| request: { |
| url: this.url, |
| }, |
| response: { |
| headers: this.responseHeaders, |
| mimeType: this.mimeType, |
| statusCode: this.statusCode, |
| statusText: this.statusText, |
| content: this._localContent, |
| base64Encoded: this._localContentIsBase64Encoded, |
| }, |
| isLocalResourceOverride: this._isLocalResourceOverride, |
| }; |
| } |
| |
| // Public |
| |
| get localContent() { return this._localContent; } |
| get localContentIsBase64Encoded() { return this._localContentIsBase64Encoded; } |
| |
| get isLocalResourceOverride() |
| { |
| return this._isLocalResourceOverride; |
| } |
| |
| hasContent() |
| { |
| return !!this._localContent; |
| } |
| |
| setContent(content, base64Encoded) |
| { |
| console.assert(!this._localContent); |
| |
| // The backend may send base64 encoded data for text resources. |
| // If that is the case decode them here and treat as text. |
| if (base64Encoded && WI.shouldTreatMIMETypeAsText(this._mimeType)) { |
| content = atob(content); |
| base64Encoded = false; |
| } |
| |
| this._localContent = content; |
| this._localContentIsBase64Encoded = base64Encoded; |
| } |
| |
| updateOverrideContent(content, base64Encoded, mimeType, options = {}) |
| { |
| console.assert(this._isLocalResourceOverride); |
| |
| if (content !== undefined && this._localContent !== content) |
| this._localContent = content; |
| |
| if (base64Encoded !== undefined && this._localContentIsBase64Encoded !== base64Encoded) |
| this._localContentIsBase64Encoded = base64Encoded; |
| |
| if (mimeType !== undefined && mimeType !== this._mimeType) { |
| let oldMIMEType = this._mimeType; |
| this._mimeType = mimeType; |
| this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType}); |
| } |
| } |
| |
| // Protected |
| |
| requestContentFromBackend() |
| { |
| return Promise.resolve({ |
| content: this._localContent, |
| base64Encoded: this._localContentIsBase64Encoded, |
| }); |
| } |
| }; |