Web Inspector: Support JSX (React) syntax highlighting
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / Resource.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  * Copyright (C) 2011 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
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  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24  * THE POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 WI.Resource = class Resource extends WI.SourceCode
28 {
29     constructor(url, mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, requestSentWalltime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp)
30     {
31         super();
32
33         console.assert(url);
34
35         if (type in WI.Resource.Type)
36             type = WI.Resource.Type[type];
37
38         this._url = url;
39         this._urlComponents = null;
40         this._mimeType = mimeType;
41         this._mimeTypeComponents = null;
42         this._type = Resource.resolvedType(type, mimeType);
43         this._loaderIdentifier = loaderIdentifier || null;
44         this._requestIdentifier = requestIdentifier || null;
45         this._queryStringParameters = undefined;
46         this._requestFormParameters = undefined;
47         this._requestMethod = requestMethod || null;
48         this._requestData = requestData || null;
49         this._requestHeaders = requestHeaders || {};
50         this._responseHeaders = {};
51         this._requestCookies = null;
52         this._responseCookies = null;
53         this._parentFrame = null;
54         this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null;
55         this._initiatedResources = [];
56         this._originalRequestWillBeSentTimestamp = originalRequestWillBeSentTimestamp || null;
57         this._requestSentTimestamp = requestSentTimestamp || NaN;
58         this._requestSentWalltime = requestSentWalltime || NaN;
59         this._responseReceivedTimestamp = NaN;
60         this._lastRedirectReceivedTimestamp = NaN;
61         this._lastDataReceivedTimestamp = NaN;
62         this._finishedOrFailedTimestamp = NaN;
63         this._finishThenRequestContentPromise = null;
64         this._statusCode = NaN;
65         this._statusText = null;
66         this._cached = false;
67         this._canceled = false;
68         this._failed = false;
69         this._failureReasonText = null;
70         this._receivedNetworkLoadMetrics = false;
71         this._responseSource = WI.Resource.ResponseSource.Unknown;
72         this._timingData = new WI.ResourceTimingData(this);
73         this._protocol = null;
74         this._priority = WI.Resource.NetworkPriority.Unknown;
75         this._remoteAddress = null;
76         this._connectionIdentifier = null;
77         this._target = targetId ? WI.targetManager.targetForIdentifier(targetId) : WI.mainTarget;
78
79         // Exact sizes if loaded over the network or cache.
80         this._requestHeadersTransferSize = NaN;
81         this._requestBodyTransferSize = NaN;
82         this._responseHeadersTransferSize = NaN;
83         this._responseBodyTransferSize = NaN;
84         this._responseBodySize = NaN;
85         this._cachedResponseBodySize = NaN;
86
87         // Estimated sizes (if backend does not provide metrics).
88         this._estimatedSize = NaN;
89         this._estimatedTransferSize = NaN;
90         this._estimatedResponseHeadersSize = NaN;
91
92         if (this._initiatorSourceCodeLocation && this._initiatorSourceCodeLocation.sourceCode instanceof WI.Resource)
93             this._initiatorSourceCodeLocation.sourceCode.addInitiatedResource(this);
94     }
95
96     // Static
97
98     static resolvedType(type, mimeType)
99     {
100         if (type && type !== WI.Resource.Type.Other)
101             return type;
102
103         return Resource.typeFromMIMEType(mimeType);
104     }
105
106     static typeFromMIMEType(mimeType)
107     {
108         if (!mimeType)
109             return WI.Resource.Type.Other;
110
111         mimeType = parseMIMEType(mimeType).type;
112
113         if (mimeType in WI.Resource._mimeTypeMap)
114             return WI.Resource._mimeTypeMap[mimeType];
115
116         if (mimeType.startsWith("image/"))
117             return WI.Resource.Type.Image;
118
119         if (mimeType.startsWith("font/"))
120             return WI.Resource.Type.Font;
121
122         return WI.Resource.Type.Other;
123     }
124
125     static displayNameForType(type, plural)
126     {
127         switch (type) {
128         case WI.Resource.Type.Document:
129             if (plural)
130                 return WI.UIString("Documents");
131             return WI.UIString("Document");
132         case WI.Resource.Type.Stylesheet:
133             if (plural)
134                 return WI.UIString("Stylesheets");
135             return WI.UIString("Stylesheet");
136         case WI.Resource.Type.Image:
137             if (plural)
138                 return WI.UIString("Images");
139             return WI.UIString("Image");
140         case WI.Resource.Type.Font:
141             if (plural)
142                 return WI.UIString("Fonts");
143             return WI.UIString("Font");
144         case WI.Resource.Type.Script:
145             if (plural)
146                 return WI.UIString("Scripts");
147             return WI.UIString("Script");
148         case WI.Resource.Type.XHR:
149             if (plural)
150                 return WI.UIString("XHRs");
151             return WI.UIString("XHR");
152         case WI.Resource.Type.Fetch:
153             if (plural)
154                 return WI.UIString("Fetches");
155             return WI.UIString("Fetch");
156         case WI.Resource.Type.Ping:
157             if (plural)
158                 return WI.UIString("Pings");
159             return WI.UIString("Ping");
160         case WI.Resource.Type.Beacon:
161             if (plural)
162                 return WI.UIString("Beacons");
163             return WI.UIString("Beacon");
164         case WI.Resource.Type.WebSocket:
165             if (plural)
166                 return WI.UIString("Sockets");
167             return WI.UIString("Socket");
168         case WI.Resource.Type.Other:
169             return WI.UIString("Other");
170         default:
171             console.error("Unknown resource type", type);
172             return null;
173         }
174     }
175
176     static displayNameForProtocol(protocol)
177     {
178         switch (protocol) {
179         case "h2":
180             return "HTTP/2";
181         case "http/1.0":
182             return "HTTP/1.0";
183         case "http/1.1":
184             return "HTTP/1.1";
185         case "spdy/2":
186             return "SPDY/2";
187         case "spdy/3":
188             return "SPDY/3";
189         case "spdy/3.1":
190             return "SPDY/3.1";
191         default:
192             return null;
193         }
194     }
195
196     static comparePriority(a, b)
197     {
198         console.assert(typeof a === "symbol");
199         console.assert(typeof b === "symbol");
200
201         const map = {
202             [WI.Resource.NetworkPriority.Unknown]: 0,
203             [WI.Resource.NetworkPriority.Low]: 1,
204             [WI.Resource.NetworkPriority.Medium]: 2,
205             [WI.Resource.NetworkPriority.High]: 3,
206         };
207
208         let aNum = map[a] || 0;
209         let bNum = map[b] || 0;
210         return aNum - bNum;
211     }
212
213     static displayNameForPriority(priority)
214     {
215         switch (priority) {
216         case WI.Resource.NetworkPriority.Low:
217             return WI.UIString("Low");
218         case WI.Resource.NetworkPriority.Medium:
219             return WI.UIString("Medium");
220         case WI.Resource.NetworkPriority.High:
221             return WI.UIString("High");
222         default:
223             return null;
224         }
225     }
226
227     static responseSourceFromPayload(source)
228     {
229         if (!source)
230             return WI.Resource.ResponseSource.Unknown;
231
232         switch (source) {
233         case NetworkAgent.ResponseSource.Unknown:
234             return WI.Resource.ResponseSource.Unknown;
235         case NetworkAgent.ResponseSource.Network:
236             return WI.Resource.ResponseSource.Network;
237         case NetworkAgent.ResponseSource.MemoryCache:
238             return WI.Resource.ResponseSource.MemoryCache;
239         case NetworkAgent.ResponseSource.DiskCache:
240             return WI.Resource.ResponseSource.DiskCache;
241         case NetworkAgent.ResponseSource.ServiceWorker:
242             return WI.Resource.ResponseSource.ServiceWorker;
243         default:
244             console.error("Unknown response source type", source);
245             return WI.Resource.ResponseSource.Unknown;
246         }
247     }
248
249     static networkPriorityFromPayload(priority)
250     {
251         switch (priority) {
252         case NetworkAgent.MetricsPriority.Low:
253             return WI.Resource.NetworkPriority.Low;
254         case NetworkAgent.MetricsPriority.Medium:
255             return WI.Resource.NetworkPriority.Medium;
256         case NetworkAgent.MetricsPriority.High:
257             return WI.Resource.NetworkPriority.High;
258         default:
259             console.error("Unknown metrics priority", priority);
260             return WI.Resource.NetworkPriority.Unknown;
261         }
262     }
263
264     static connectionIdentifierFromPayload(connectionIdentifier)
265     {
266         // Map backend connection identifiers to an easier to read number.
267         if (!WI.Resource.connectionIdentifierMap) {
268             WI.Resource.connectionIdentifierMap = new Map;
269             WI.Resource.nextConnectionIdentifier = 1;
270         }
271
272         let id = WI.Resource.connectionIdentifierMap.get(connectionIdentifier);
273         if (id)
274             return id;
275
276         id = WI.Resource.nextConnectionIdentifier++;
277         WI.Resource.connectionIdentifierMap.set(connectionIdentifier, id);
278         return id;
279     }
280
281     // Public
282
283     get target() { return this._target; }
284     get type() { return this._type; }
285     get loaderIdentifier() { return this._loaderIdentifier; }
286     get requestIdentifier() { return this._requestIdentifier; }
287     get requestMethod() { return this._requestMethod; }
288     get requestData() { return this._requestData; }
289     get statusCode() { return this._statusCode; }
290     get statusText() { return this._statusText; }
291     get responseSource() { return this._responseSource; }
292     get timingData() { return this._timingData; }
293     get protocol() { return this._protocol; }
294     get priority() { return this._priority; }
295     get remoteAddress() { return this._remoteAddress; }
296     get connectionIdentifier() { return this._connectionIdentifier; }
297
298     get url()
299     {
300         return this._url;
301     }
302
303     get urlComponents()
304     {
305         if (!this._urlComponents)
306             this._urlComponents = parseURL(this._url);
307         return this._urlComponents;
308     }
309
310     get displayName()
311     {
312         return WI.displayNameForURL(this._url, this.urlComponents);
313     }
314
315     get displayURL()
316     {
317         const isMultiLine = true;
318         const dataURIMaxSize = 64;
319         return WI.truncateURL(this._url, isMultiLine, dataURIMaxSize);
320     }
321
322     get initiatorSourceCodeLocation()
323     {
324         return this._initiatorSourceCodeLocation;
325     }
326
327     get initiatedResources()
328     {
329         return this._initiatedResources;
330     }
331
332     get originalRequestWillBeSentTimestamp()
333     {
334         return this._originalRequestWillBeSentTimestamp;
335     }
336
337     get mimeType()
338     {
339         return this._mimeType;
340     }
341
342     get mimeTypeComponents()
343     {
344         if (!this._mimeTypeComponents)
345             this._mimeTypeComponents = parseMIMEType(this._mimeType);
346         return this._mimeTypeComponents;
347     }
348
349     get syntheticMIMEType()
350     {
351         // Resources are often transferred with a MIME-type that doesn't match the purpose the
352         // resource was loaded for, which is what WI.Resource.Type represents.
353         // This getter generates a MIME-type, if needed, that matches the resource type.
354
355         // If the type matches the Resource.Type of the MIME-type, then return the actual MIME-type.
356         if (this._type === WI.Resource.typeFromMIMEType(this._mimeType))
357             return this._mimeType;
358
359         // Return the default MIME-types for the Resource.Type, since the current MIME-type
360         // does not match what is expected for the Resource.Type.
361         switch (this._type) {
362         case WI.Resource.Type.Stylesheet:
363             return "text/css";
364         case WI.Resource.Type.Script:
365             return "text/javascript";
366         }
367
368         // Return the actual MIME-type since we don't have a better synthesized one to return.
369         return this._mimeType;
370     }
371
372     createObjectURL()
373     {
374         // If content is not available, fallback to using original URL.
375         // The client may try to revoke it, but nothing will happen.
376         let content = this.content;
377         if (!content)
378             return this._url;
379
380         if (content instanceof Blob)
381             return URL.createObjectURL(content);
382
383         if (typeof content === "string") {
384             let blob = textToBlob(content, this._mimeType);
385             return URL.createObjectURL(blob);
386         }
387
388         return null;
389     }
390
391     isMainResource()
392     {
393         return this._parentFrame ? this._parentFrame.mainResource === this : false;
394     }
395
396     addInitiatedResource(resource)
397     {
398         if (!(resource instanceof WI.Resource))
399             return;
400
401         this._initiatedResources.push(resource);
402
403         this.dispatchEventToListeners(WI.Resource.Event.InitiatedResourcesDidChange);
404     }
405
406     get parentFrame()
407     {
408         return this._parentFrame;
409     }
410
411     get finished()
412     {
413         return this._finished;
414     }
415
416     get failed()
417     {
418         return this._failed;
419     }
420
421     get canceled()
422     {
423         return this._canceled;
424     }
425
426     get failureReasonText()
427     {
428         return this._failureReasonText;
429     }
430
431     get queryStringParameters()
432     {
433         if (this._queryStringParameters === undefined)
434             this._queryStringParameters = parseQueryString(this.urlComponents.queryString, true);
435         return this._queryStringParameters;
436     }
437
438     get requestFormParameters()
439     {
440         if (this._requestFormParameters === undefined)
441             this._requestFormParameters = this.hasRequestFormParameters() ? parseQueryString(this.requestData, true) : null;
442         return this._requestFormParameters;
443     }
444
445     get requestDataContentType()
446     {
447         return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null;
448     }
449
450     get requestHeaders()
451     {
452         return this._requestHeaders;
453     }
454
455     get responseHeaders()
456     {
457         return this._responseHeaders;
458     }
459
460     get requestCookies()
461     {
462         if (!this._requestCookies)
463             this._requestCookies = WI.Cookie.parseCookieRequestHeader(this._requestHeaders.valueForCaseInsensitiveKey("Cookie"));
464
465         return this._requestCookies;
466     }
467
468     get responseCookies()
469     {
470         if (!this._responseCookies) {
471             // FIXME: The backend sends multiple "Set-Cookie" headers in one "Set-Cookie" with multiple values
472             // separated by ", ". This doesn't allow us to safely distinguish between a ", " that separates
473             // multiple headers or one that may be valid part of a Cookie's value or attribute, such as the
474             // ", " in the the date format "Expires=Tue, 03-Oct-2017 04:39:21 GMT". To improve heuristics
475             // we do a negative lookahead for numbers, but we can still fail on cookie values containing ", ".
476             let rawCombinedHeader = this._responseHeaders.valueForCaseInsensitiveKey("Set-Cookie") || "";
477             let setCookieHeaders = rawCombinedHeader.split(/, (?![0-9])/);
478             let cookies = [];
479             for (let header of setCookieHeaders) {
480                 let cookie = WI.Cookie.parseSetCookieResponseHeader(header);
481                 if (cookie)
482                     cookies.push(cookie);
483             }
484             this._responseCookies = cookies;
485         }
486
487         return this._responseCookies;
488     }
489
490     get requestSentTimestamp()
491     {
492         return this._requestSentTimestamp;
493     }
494
495     get requestSentWalltime()
496     {
497         return this._requestSentWalltime;
498     }
499
500     get requestSentDate()
501     {
502         return isNaN(this._requestSentWalltime) ? null : new Date(this._requestSentWalltime * 1000);
503     }
504
505     get lastRedirectReceivedTimestamp()
506     {
507         return this._lastRedirectReceivedTimestamp;
508     }
509
510     get responseReceivedTimestamp()
511     {
512         return this._responseReceivedTimestamp;
513     }
514
515     get lastDataReceivedTimestamp()
516     {
517         return this._lastDataReceivedTimestamp;
518     }
519
520     get finishedOrFailedTimestamp()
521     {
522         return this._finishedOrFailedTimestamp;
523     }
524
525     get firstTimestamp()
526     {
527         return this.timingData.startTime || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp;
528     }
529
530     get lastTimestamp()
531     {
532         return this.timingData.responseEnd || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp;
533     }
534
535     get latency()
536     {
537         return this.timingData.responseStart - this.timingData.requestStart;
538     }
539
540     get receiveDuration()
541     {
542         return this.timingData.responseEnd - this.timingData.responseStart;
543     }
544
545     get totalDuration()
546     {
547         return this.timingData.responseEnd - this.timingData.startTime;
548     }
549
550     get cached()
551     {
552         return this._cached;
553     }
554
555     get requestHeadersTransferSize() { return this._requestHeadersTransferSize; }
556     get requestBodyTransferSize() { return this._requestBodyTransferSize; }
557     get responseHeadersTransferSize() { return this._responseHeadersTransferSize; }
558     get responseBodyTransferSize() { return this._responseBodyTransferSize; }
559     get cachedResponseBodySize() { return this._cachedResponseBodySize; }
560
561     get size()
562     {
563         if (!isNaN(this._cachedResponseBodySize))
564             return this._cachedResponseBodySize;
565
566         if (!isNaN(this._responseBodySize) && this._responseBodySize !== 0)
567             return this._responseBodySize;
568
569         return this._estimatedSize;
570     }
571
572     get networkEncodedSize()
573     {
574         return this._responseBodyTransferSize;
575     }
576
577     get networkDecodedSize()
578     {
579         return this._responseBodySize;
580     }
581
582     get networkTotalTransferSize()
583     {
584         return this._responseHeadersTransferSize + this._responseBodyTransferSize;
585     }
586
587     get estimatedNetworkEncodedSize()
588     {
589         let exact = this.networkEncodedSize;
590         if (!isNaN(exact))
591             return exact;
592
593         if (this._cached)
594             return 0;
595
596         // FIXME: <https://webkit.org/b/158463> Network: Correctly report encoded data length (transfer size) from CFNetwork to NetworkResourceLoader
597         // macOS provides the decoded transfer size instead of the encoded size
598         // for estimatedTransferSize. So prefer the "Content-Length" property
599         // on mac if it is available.
600         if (WI.Platform.name === "mac") {
601             let contentLength = Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"));
602             if (!isNaN(contentLength))
603                 return contentLength;
604         }
605
606         if (!isNaN(this._estimatedTransferSize))
607             return this._estimatedTransferSize;
608
609         // If we did not receive actual transfer size from network
610         // stack, we prefer using Content-Length over resourceSize as
611         // resourceSize may differ from actual transfer size if platform's
612         // network stack performed decoding (e.g. gzip decompression).
613         // The Content-Length, though, is expected to come from raw
614         // response headers and will reflect actual transfer length.
615         // This won't work for chunked content encoding, so fall back to
616         // resourceSize when we don't have Content-Length. This still won't
617         // work for chunks with non-trivial encodings. We need a way to
618         // get actual transfer size from the network stack.
619
620         return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._estimatedSize);
621     }
622
623     get estimatedTotalTransferSize()
624     {
625         let exact = this.networkTotalTransferSize;
626         if (!isNaN(exact))
627             return exact;
628
629         if (this.statusCode === 304) // Not modified
630             return this._estimatedResponseHeadersSize;
631
632         if (this._cached)
633             return 0;
634
635         return this._estimatedResponseHeadersSize + this.estimatedNetworkEncodedSize;
636     }
637
638     get compressed()
639     {
640         let contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
641         return !!(contentEncoding && /\b(?:gzip|deflate)\b/.test(contentEncoding));
642     }
643
644     get scripts()
645     {
646         return this._scripts || [];
647     }
648
649     scriptForLocation(sourceCodeLocation)
650     {
651         console.assert(!(this instanceof WI.SourceMapResource));
652         console.assert(sourceCodeLocation.sourceCode === this, "SourceCodeLocation must be in this Resource");
653         if (sourceCodeLocation.sourceCode !== this)
654             return null;
655
656         var lineNumber = sourceCodeLocation.lineNumber;
657         var columnNumber = sourceCodeLocation.columnNumber;
658         for (var i = 0; i < this._scripts.length; ++i) {
659             var script = this._scripts[i];
660             if (script.range.startLine <= lineNumber && script.range.endLine >= lineNumber) {
661                 if (script.range.startLine === lineNumber && columnNumber < script.range.startColumn)
662                     continue;
663                 if (script.range.endLine === lineNumber && columnNumber > script.range.endColumn)
664                     continue;
665                 return script;
666             }
667         }
668
669         return null;
670     }
671
672     updateForRedirectResponse(url, requestHeaders, elapsedTime)
673     {
674         console.assert(!this._finished);
675         console.assert(!this._failed);
676         console.assert(!this._canceled);
677
678         var oldURL = this._url;
679
680         if (url)
681             this._url = url;
682
683         this._requestHeaders = requestHeaders || {};
684         this._requestCookies = null;
685         this._lastRedirectReceivedTimestamp = elapsedTime || NaN;
686
687         if (oldURL !== url) {
688             // Delete the URL components so the URL is re-parsed the next time it is requested.
689             this._urlComponents = null;
690
691             this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
692         }
693
694         this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
695         this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
696     }
697
698     hasResponse()
699     {
700         return !isNaN(this._statusCode) || this._finished || this._failed;
701     }
702
703     hasRequestFormParameters()
704     {
705         let requestDataContentType = this.requestDataContentType;
706         return requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i);
707     }
708
709     updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, elapsedTime, timingData, source)
710     {
711         console.assert(!this._finished);
712         console.assert(!this._failed);
713         console.assert(!this._canceled);
714
715         let oldURL = this._url;
716         let oldMIMEType = this._mimeType;
717         let oldType = this._type;
718
719         if (type in WI.Resource.Type)
720             type = WI.Resource.Type[type];
721
722         if (url)
723             this._url = url;
724
725         this._mimeType = mimeType;
726         this._type = Resource.resolvedType(type, mimeType);
727         this._statusCode = statusCode;
728         this._statusText = statusText;
729         this._responseHeaders = responseHeaders || {};
730         this._responseCookies = null;
731         this._responseReceivedTimestamp = elapsedTime || NaN;
732         this._timingData = WI.ResourceTimingData.fromPayload(timingData, this);
733
734         if (source)
735             this._responseSource = WI.Resource.responseSourceFromPayload(source);
736
737         const headerBaseSize = 12; // Length of "HTTP/1.1 ", " ", and "\r\n".
738         const headerPad = 4; // Length of ": " and "\r\n".
739         this._estimatedResponseHeadersSize = String(this._statusCode).length + this._statusText.length + headerBaseSize;
740         for (let name in this._responseHeaders)
741             this._estimatedResponseHeadersSize += name.length + this._responseHeaders[name].length + headerPad;
742
743         if (!this._cached) {
744             if (statusCode === 304 || (this._responseSource === WI.Resource.ResponseSource.MemoryCache || this._responseSource === WI.Resource.ResponseSource.DiskCache))
745                 this.markAsCached();
746         }
747
748         if (oldURL !== url) {
749             // Delete the URL components so the URL is re-parsed the next time it is requested.
750             this._urlComponents = null;
751
752             this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
753         }
754
755         if (oldMIMEType !== mimeType) {
756             // Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested.
757             this._mimeTypeComponents = null;
758
759             this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType});
760         }
761
762         if (oldType !== type)
763             this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
764
765         console.assert(isNaN(this._estimatedSize));
766         console.assert(isNaN(this._estimatedTransferSize));
767
768         // The transferSize becomes 0 when status is 304 or Content-Length is available, so
769         // notify listeners of that change.
770         if (statusCode === 304 || this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
771             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
772
773         this.dispatchEventToListeners(WI.Resource.Event.ResponseReceived);
774         this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
775     }
776
777     updateWithMetrics(metrics)
778     {
779         this._receivedNetworkLoadMetrics = true;
780
781         if (metrics.protocol)
782             this._protocol = metrics.protocol;
783         if (metrics.priority)
784             this._priority = WI.Resource.networkPriorityFromPayload(metrics.priority);
785         if (metrics.remoteAddress)
786             this._remoteAddress = metrics.remoteAddress;
787         if (metrics.connectionIdentifier)
788             this._connectionIdentifier = WI.Resource.connectionIdentifierFromPayload(metrics.connectionIdentifier);
789         if (metrics.requestHeaders) {
790             this._requestHeaders = metrics.requestHeaders;
791             this._requestCookies = null;
792             this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
793         }
794
795         if ("requestHeaderBytesSent" in metrics) {
796             this._requestHeadersTransferSize = metrics.requestHeaderBytesSent;
797             this._requestBodyTransferSize = metrics.requestBodyBytesSent;
798             this._responseHeadersTransferSize = metrics.responseHeaderBytesReceived;
799             this._responseBodyTransferSize = metrics.responseBodyBytesReceived;
800             this._responseBodySize = metrics.responseBodyDecodedSize;
801
802             console.assert(this._requestHeadersTransferSize >= 0);
803             console.assert(this._requestBodyTransferSize >= 0);
804             console.assert(this._responseHeadersTransferSize >= 0);
805             console.assert(this._responseBodyTransferSize >= 0);
806             console.assert(this._responseBodySize >= 0);
807
808             this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize: this._estimatedSize});
809             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
810         }
811
812         this.dispatchEventToListeners(WI.Resource.Event.MetricsDidChange);
813     }
814
815     setCachedResponseBodySize(size)
816     {
817         console.assert(!isNaN(size), "Size should be a valid number.");
818         console.assert(isNaN(this._cachedResponseBodySize), "This should only be set once.");
819         console.assert(this._estimatedSize === size, "The legacy path was updated already and matches.");
820
821         this._cachedResponseBodySize = size;
822     }
823
824     requestContentFromBackend()
825     {
826         // If we have the requestIdentifier we can get the actual response for this specific resource.
827         // Otherwise the content will be cached resource data, which might not exist anymore.
828         if (this._requestIdentifier)
829             return NetworkAgent.getResponseBody(this._requestIdentifier);
830
831         // There is no request identifier or frame to request content from.
832         if (this._parentFrame)
833             return PageAgent.getResourceContent(this._parentFrame.id, this._url);
834
835         return Promise.reject(new Error("Content request failed."));
836     }
837
838     increaseSize(dataLength, elapsedTime)
839     {
840         console.assert(dataLength >= 0);
841         console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated size.");
842
843         if (isNaN(this._estimatedSize))
844             this._estimatedSize = 0;
845
846         let previousSize = this._estimatedSize;
847
848         this._estimatedSize += dataLength;
849
850         this._lastDataReceivedTimestamp = elapsedTime || NaN;
851
852         this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize});
853
854         // The estimatedTransferSize is based off of size when status is not 304 or Content-Length is missing.
855         if (isNaN(this._estimatedTransferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
856             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
857     }
858
859     increaseTransferSize(encodedDataLength)
860     {
861         console.assert(encodedDataLength >= 0);
862         console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated transfer size.");
863
864         if (isNaN(this._estimatedTransferSize))
865             this._estimatedTransferSize = 0;
866         this._estimatedTransferSize += encodedDataLength;
867
868         this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
869     }
870
871     markAsCached()
872     {
873         this._cached = true;
874
875         this.dispatchEventToListeners(WI.Resource.Event.CacheStatusDidChange);
876
877         // The transferSize starts returning 0 when cached is true, unless status is 304.
878         if (this._statusCode !== 304)
879             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
880     }
881
882     markAsFinished(elapsedTime)
883     {
884         console.assert(!this._failed);
885         console.assert(!this._canceled);
886
887         this._finished = true;
888         this._finishedOrFailedTimestamp = elapsedTime || NaN;
889         this._timingData.markResponseEndTime(elapsedTime || NaN);
890
891         if (this._finishThenRequestContentPromise)
892             this._finishThenRequestContentPromise = null;
893
894         this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFinish);
895         this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
896     }
897
898     markAsFailed(canceled, elapsedTime, errorText)
899     {
900         console.assert(!this._finished);
901
902         this._failed = true;
903         this._canceled = canceled;
904         this._finishedOrFailedTimestamp = elapsedTime || NaN;
905
906         if (!this._failureReasonText)
907             this._failureReasonText = errorText || null;
908
909         this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFail);
910         this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
911     }
912
913     revertMarkAsFinished()
914     {
915         console.assert(!this._failed);
916         console.assert(!this._canceled);
917         console.assert(this._finished);
918
919         this._finished = false;
920         this._finishedOrFailedTimestamp = NaN;
921     }
922
923     legacyMarkServedFromMemoryCache()
924     {
925         // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the MemoryCache.
926         console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown);
927
928         this._responseSource = WI.Resource.ResponseSource.MemoryCache;
929
930         this.markAsCached();
931     }
932
933     legacyMarkServedFromDiskCache()
934     {
935         // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the DiskCache.
936         console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown);
937
938         this._responseSource = WI.Resource.ResponseSource.DiskCache;
939
940         this.markAsCached();
941     }
942
943     isLoading()
944     {
945         return !this._finished && !this._failed;
946     }
947
948     hadLoadingError()
949     {
950         return this._failed || this._canceled || this._statusCode >= 400;
951     }
952
953     getImageSize(callback)
954     {
955         // Throw an error in the case this resource is not an image.
956         if (this.type !== WI.Resource.Type.Image)
957             throw "Resource is not an image.";
958
959         // See if we've already computed and cached the image size,
960         // in which case we can provide them directly.
961         if (this._imageSize !== undefined) {
962             callback(this._imageSize);
963             return;
964         }
965
966         var objectURL = null;
967
968         // Event handler for the image "load" event.
969         function imageDidLoad() {
970             URL.revokeObjectURL(objectURL);
971
972             // Cache the image metrics.
973             this._imageSize = {
974                 width: image.width,
975                 height: image.height
976             };
977
978             callback(this._imageSize);
979         }
980
981         function requestContentFailure() {
982             this._imageSize = null;
983             callback(this._imageSize);
984         }
985
986         // Create an <img> element that we'll use to load the image resource
987         // so that we can query its intrinsic size.
988         var image = new Image;
989         image.addEventListener("load", imageDidLoad.bind(this), false);
990
991         // Set the image source using an object URL once we've obtained its data.
992         this.requestContent().then((content) => {
993             objectURL = image.src = content.sourceCode.createObjectURL();
994             if (!objectURL)
995                 requestContentFailure.call(this);
996         }, requestContentFailure.bind(this));
997     }
998
999     requestContent()
1000     {
1001         if (this._finished)
1002             return super.requestContent();
1003
1004         if (this._failed)
1005             return Promise.resolve({error: WI.UIString("An error occurred trying to load the resource.")});
1006
1007         if (!this._finishThenRequestContentPromise) {
1008             this._finishThenRequestContentPromise = new Promise((resolve, reject) => {
1009                 this.addEventListener(WI.Resource.Event.LoadingDidFinish, resolve);
1010                 this.addEventListener(WI.Resource.Event.LoadingDidFail, reject);
1011             }).then(WI.SourceCode.prototype.requestContent.bind(this));
1012         }
1013
1014         return this._finishThenRequestContentPromise;
1015     }
1016
1017     associateWithScript(script)
1018     {
1019         if (!this._scripts)
1020             this._scripts = [];
1021
1022         this._scripts.push(script);
1023
1024         if (this._type === WI.Resource.Type.Other || this._type === WI.Resource.Type.XHR) {
1025             let oldType = this._type;
1026             this._type = WI.Resource.Type.Script;
1027             this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
1028         }
1029     }
1030
1031     saveIdentityToCookie(cookie)
1032     {
1033         cookie[WI.Resource.URLCookieKey] = this.url.hash;
1034         cookie[WI.Resource.MainResourceCookieKey] = this.isMainResource();
1035     }
1036
1037     generateCURLCommand()
1038     {
1039         function escapeStringPosix(str) {
1040             function escapeCharacter(x) {
1041                 let code = x.charCodeAt(0);
1042                 let hex = code.toString(16);
1043                 if (code < 256)
1044                     return "\\x" + hex.padStart(2, "0");
1045                 return "\\u" + hex.padStart(4, "0");
1046             }
1047
1048             if (/[^\x20-\x7E]|'/.test(str)) {
1049                 // Use ANSI-C quoting syntax.
1050                 return "$'" + str.replace(/\\/g, "\\\\")
1051                                  .replace(/'/g, "\\'")
1052                                  .replace(/\n/g, "\\n")
1053                                  .replace(/\r/g, "\\r")
1054                                  .replace(/!/g, "\\041")
1055                                  .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
1056             } else {
1057                 // Use single quote syntax.
1058                 return `'${str}'`;
1059             }
1060         }
1061
1062         let command = ["curl " + escapeStringPosix(this.url).replace(/[[{}\]]/g, "\\$&")];
1063         command.push(`-X${this.requestMethod}`);
1064
1065         for (let key in this.requestHeaders)
1066             command.push("-H " + escapeStringPosix(`${key}: ${this.requestHeaders[key]}`));
1067
1068         if (this.requestDataContentType && this.requestMethod !== "GET" && this.requestData) {
1069             if (this.requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
1070                 command.push("--data " + escapeStringPosix(this.requestData));
1071             else
1072                 command.push("--data-binary " + escapeStringPosix(this.requestData));
1073         }
1074
1075         let curlCommand = command.join(" \\\n");
1076         InspectorFrontendHost.copyText(curlCommand);
1077         return curlCommand;
1078     }
1079 };
1080
1081 WI.Resource.TypeIdentifier = "resource";
1082 WI.Resource.URLCookieKey = "resource-url";
1083 WI.Resource.MainResourceCookieKey = "resource-is-main-resource";
1084
1085 WI.Resource.Event = {
1086     URLDidChange: "resource-url-did-change",
1087     MIMETypeDidChange: "resource-mime-type-did-change",
1088     TypeDidChange: "resource-type-did-change",
1089     RequestHeadersDidChange: "resource-request-headers-did-change",
1090     ResponseReceived: "resource-response-received",
1091     LoadingDidFinish: "resource-loading-did-finish",
1092     LoadingDidFail: "resource-loading-did-fail",
1093     TimestampsDidChange: "resource-timestamps-did-change",
1094     SizeDidChange: "resource-size-did-change",
1095     TransferSizeDidChange: "resource-transfer-size-did-change",
1096     CacheStatusDidChange: "resource-cached-did-change",
1097     MetricsDidChange: "resource-metrics-did-change",
1098     InitiatedResourcesDidChange: "resource-initiated-resources-did-change",
1099 };
1100
1101 // Keep these in sync with the "ResourceType" enum defined by the "Page" domain.
1102 WI.Resource.Type = {
1103     Document: "resource-type-document",
1104     Stylesheet: "resource-type-stylesheet",
1105     Image: "resource-type-image",
1106     Font: "resource-type-font",
1107     Script: "resource-type-script",
1108     XHR: "resource-type-xhr",
1109     Fetch: "resource-type-fetch",
1110     Ping: "resource-type-ping",
1111     Beacon: "resource-type-beacon",
1112     WebSocket: "resource-type-websocket",
1113     Other: "resource-type-other",
1114 };
1115
1116 WI.Resource.ResponseSource = {
1117     Unknown: Symbol("unknown"),
1118     Network: Symbol("network"),
1119     MemoryCache: Symbol("memory-cache"),
1120     DiskCache: Symbol("disk-cache"),
1121     ServiceWorker: Symbol("service-worker"),
1122 };
1123
1124 WI.Resource.NetworkPriority = {
1125     Unknown: Symbol("unknown"),
1126     Low: Symbol("low"),
1127     Medium: Symbol("medium"),
1128     High: Symbol("high"),
1129 };
1130
1131 // This MIME Type map is private, use WI.Resource.typeFromMIMEType().
1132 WI.Resource._mimeTypeMap = {
1133     "text/html": WI.Resource.Type.Document,
1134     "text/xml": WI.Resource.Type.Document,
1135     "text/plain": WI.Resource.Type.Document,
1136     "application/xhtml+xml": WI.Resource.Type.Document,
1137
1138     "text/css": WI.Resource.Type.Stylesheet,
1139     "text/xsl": WI.Resource.Type.Stylesheet,
1140     "text/x-less": WI.Resource.Type.Stylesheet,
1141     "text/x-sass": WI.Resource.Type.Stylesheet,
1142     "text/x-scss": WI.Resource.Type.Stylesheet,
1143
1144     "application/pdf": WI.Resource.Type.Image,
1145     "image/svg+xml": WI.Resource.Type.Image,
1146
1147     "application/x-font-type1": WI.Resource.Type.Font,
1148     "application/x-font-ttf": WI.Resource.Type.Font,
1149     "application/x-font-woff": WI.Resource.Type.Font,
1150     "application/x-truetype-font": WI.Resource.Type.Font,
1151
1152     "text/javascript": WI.Resource.Type.Script,
1153     "text/ecmascript": WI.Resource.Type.Script,
1154     "application/javascript": WI.Resource.Type.Script,
1155     "application/ecmascript": WI.Resource.Type.Script,
1156     "application/x-javascript": WI.Resource.Type.Script,
1157     "application/json": WI.Resource.Type.Script,
1158     "application/x-json": WI.Resource.Type.Script,
1159     "text/x-javascript": WI.Resource.Type.Script,
1160     "text/x-json": WI.Resource.Type.Script,
1161     "text/javascript1.1": WI.Resource.Type.Script,
1162     "text/javascript1.2": WI.Resource.Type.Script,
1163     "text/javascript1.3": WI.Resource.Type.Script,
1164     "text/jscript": WI.Resource.Type.Script,
1165     "text/livescript": WI.Resource.Type.Script,
1166     "text/x-livescript": WI.Resource.Type.Script,
1167     "text/typescript": WI.Resource.Type.Script,
1168     "text/typescript-jsx": WI.Resource.Type.Script,
1169     "text/jsx": WI.Resource.Type.Script,
1170     "text/x-clojure": WI.Resource.Type.Script,
1171     "text/x-coffeescript": WI.Resource.Type.Script,
1172 };