ServiceWorker Inspector: Frontend changes to support Network tab and sub resources
[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         return null;
384     }
385
386     isMainResource()
387     {
388         return this._parentFrame ? this._parentFrame.mainResource === this : false;
389     }
390
391     addInitiatedResource(resource)
392     {
393         if (!(resource instanceof WI.Resource))
394             return;
395
396         this._initiatedResources.push(resource);
397
398         this.dispatchEventToListeners(WI.Resource.Event.InitiatedResourcesDidChange);
399     }
400
401     get parentFrame()
402     {
403         return this._parentFrame;
404     }
405
406     get finished()
407     {
408         return this._finished;
409     }
410
411     get failed()
412     {
413         return this._failed;
414     }
415
416     get canceled()
417     {
418         return this._canceled;
419     }
420
421     get failureReasonText()
422     {
423         return this._failureReasonText;
424     }
425
426     get queryStringParameters()
427     {
428         if (this._queryStringParameters === undefined)
429             this._queryStringParameters = parseQueryString(this.urlComponents.queryString, true);
430         return this._queryStringParameters;
431     }
432
433     get requestFormParameters()
434     {
435         if (this._requestFormParameters === undefined)
436             this._requestFormParameters = this.hasRequestFormParameters() ? parseQueryString(this.requestData, true) : null;
437         return this._requestFormParameters;
438     }
439
440     get requestDataContentType()
441     {
442         return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null;
443     }
444
445     get requestHeaders()
446     {
447         return this._requestHeaders;
448     }
449
450     get responseHeaders()
451     {
452         return this._responseHeaders;
453     }
454
455     get requestCookies()
456     {
457         if (!this._requestCookies)
458             this._requestCookies = WI.Cookie.parseCookieRequestHeader(this._requestHeaders.valueForCaseInsensitiveKey("Cookie"));
459
460         return this._requestCookies;
461     }
462
463     get responseCookies()
464     {
465         if (!this._responseCookies) {
466             // FIXME: The backend sends multiple "Set-Cookie" headers in one "Set-Cookie" with multiple values
467             // separated by ", ". This doesn't allow us to safely distinguish between a ", " that separates
468             // multiple headers or one that may be valid part of a Cookie's value or attribute, such as the
469             // ", " in the the date format "Expires=Tue, 03-Oct-2017 04:39:21 GMT". To improve heuristics
470             // we do a negative lookahead for numbers, but we can still fail on cookie values containing ", ".
471             let rawCombinedHeader = this._responseHeaders.valueForCaseInsensitiveKey("Set-Cookie") || "";
472             let setCookieHeaders = rawCombinedHeader.split(/, (?![0-9])/);
473             let cookies = [];
474             for (let header of setCookieHeaders) {
475                 let cookie = WI.Cookie.parseSetCookieResponseHeader(header);
476                 if (cookie)
477                     cookies.push(cookie);
478             }
479             this._responseCookies = cookies;
480         }
481
482         return this._responseCookies;
483     }
484
485     get requestSentTimestamp()
486     {
487         return this._requestSentTimestamp;
488     }
489
490     get requestSentWalltime()
491     {
492         return this._requestSentWalltime;
493     }
494
495     get requestSentDate()
496     {
497         return isNaN(this._requestSentWalltime) ? null : new Date(this._requestSentWalltime * 1000);
498     }
499
500     get lastRedirectReceivedTimestamp()
501     {
502         return this._lastRedirectReceivedTimestamp;
503     }
504
505     get responseReceivedTimestamp()
506     {
507         return this._responseReceivedTimestamp;
508     }
509
510     get lastDataReceivedTimestamp()
511     {
512         return this._lastDataReceivedTimestamp;
513     }
514
515     get finishedOrFailedTimestamp()
516     {
517         return this._finishedOrFailedTimestamp;
518     }
519
520     get firstTimestamp()
521     {
522         return this.timingData.startTime || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp;
523     }
524
525     get lastTimestamp()
526     {
527         return this.timingData.responseEnd || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp;
528     }
529
530     get duration()
531     {
532         return this.timingData.responseEnd - this.timingData.requestStart;
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 cached()
546     {
547         return this._cached;
548     }
549
550     get requestHeadersTransferSize() { return this._requestHeadersTransferSize; }
551     get requestBodyTransferSize() { return this._requestBodyTransferSize; }
552     get responseHeadersTransferSize() { return this._responseHeadersTransferSize; }
553     get responseBodyTransferSize() { return this._responseBodyTransferSize; }
554     get cachedResponseBodySize() { return this._cachedResponseBodySize; }
555
556     get size()
557     {
558         if (!isNaN(this._cachedResponseBodySize))
559             return this._cachedResponseBodySize;
560
561         if (!isNaN(this._responseBodySize) && this._responseBodySize !== 0)
562             return this._responseBodySize;
563
564         return this._estimatedSize;
565     }
566
567     get networkEncodedSize()
568     {
569         return this._responseBodyTransferSize;
570     }
571
572     get networkDecodedSize()
573     {
574         return this._responseBodySize;
575     }
576
577     get networkTotalTransferSize()
578     {
579         return this._responseHeadersTransferSize + this._responseBodyTransferSize;
580     }
581
582     get estimatedNetworkEncodedSize()
583     {
584         let exact = this.networkEncodedSize;
585         if (!isNaN(exact))
586             return exact;
587
588         if (this._cached)
589             return 0;
590
591         // FIXME: <https://webkit.org/b/158463> Network: Correctly report encoded data length (transfer size) from CFNetwork to NetworkResourceLoader
592         // macOS provides the decoded transfer size instead of the encoded size
593         // for estimatedTransferSize. So prefer the "Content-Length" property
594         // on mac if it is available.
595         if (WI.Platform.name === "mac") {
596             let contentLength = Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"));
597             if (!isNaN(contentLength))
598                 return contentLength;
599         }
600
601         if (!isNaN(this._estimatedTransferSize))
602             return this._estimatedTransferSize;
603
604         // If we did not receive actual transfer size from network
605         // stack, we prefer using Content-Length over resourceSize as
606         // resourceSize may differ from actual transfer size if platform's
607         // network stack performed decoding (e.g. gzip decompression).
608         // The Content-Length, though, is expected to come from raw
609         // response headers and will reflect actual transfer length.
610         // This won't work for chunked content encoding, so fall back to
611         // resourceSize when we don't have Content-Length. This still won't
612         // work for chunks with non-trivial encodings. We need a way to
613         // get actual transfer size from the network stack.
614
615         return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._estimatedSize);
616     }
617
618     get estimatedTotalTransferSize()
619     {
620         let exact = this.networkTotalTransferSize;
621         if (!isNaN(exact))
622             return exact;
623
624         if (this.statusCode === 304) // Not modified
625             return this._estimatedResponseHeadersSize;
626
627         if (this._cached)
628             return 0;
629
630         return this._estimatedResponseHeadersSize + this.estimatedNetworkEncodedSize;
631     }
632
633     get compressed()
634     {
635         let contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
636         return !!(contentEncoding && /\b(?:gzip|deflate)\b/.test(contentEncoding));
637     }
638
639     get scripts()
640     {
641         return this._scripts || [];
642     }
643
644     scriptForLocation(sourceCodeLocation)
645     {
646         console.assert(!(this instanceof WI.SourceMapResource));
647         console.assert(sourceCodeLocation.sourceCode === this, "SourceCodeLocation must be in this Resource");
648         if (sourceCodeLocation.sourceCode !== this)
649             return null;
650
651         var lineNumber = sourceCodeLocation.lineNumber;
652         var columnNumber = sourceCodeLocation.columnNumber;
653         for (var i = 0; i < this._scripts.length; ++i) {
654             var script = this._scripts[i];
655             if (script.range.startLine <= lineNumber && script.range.endLine >= lineNumber) {
656                 if (script.range.startLine === lineNumber && columnNumber < script.range.startColumn)
657                     continue;
658                 if (script.range.endLine === lineNumber && columnNumber > script.range.endColumn)
659                     continue;
660                 return script;
661             }
662         }
663
664         return null;
665     }
666
667     updateForRedirectResponse(url, requestHeaders, elapsedTime)
668     {
669         console.assert(!this._finished);
670         console.assert(!this._failed);
671         console.assert(!this._canceled);
672
673         var oldURL = this._url;
674
675         if (url)
676             this._url = url;
677
678         this._requestHeaders = requestHeaders || {};
679         this._requestCookies = null;
680         this._lastRedirectReceivedTimestamp = elapsedTime || NaN;
681
682         if (oldURL !== url) {
683             // Delete the URL components so the URL is re-parsed the next time it is requested.
684             this._urlComponents = null;
685
686             this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
687         }
688
689         this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
690         this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
691     }
692
693     hasResponse()
694     {
695         return !isNaN(this._statusCode) || this._finished || this._failed;
696     }
697
698     hasRequestFormParameters()
699     {
700         let requestDataContentType = this.requestDataContentType;
701         return requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i);
702     }
703
704     updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, elapsedTime, timingData, source)
705     {
706         console.assert(!this._finished);
707         console.assert(!this._failed);
708         console.assert(!this._canceled);
709
710         let oldURL = this._url;
711         let oldMIMEType = this._mimeType;
712         let oldType = this._type;
713
714         if (type in WI.Resource.Type)
715             type = WI.Resource.Type[type];
716
717         if (url)
718             this._url = url;
719
720         this._mimeType = mimeType;
721         this._type = Resource.resolvedType(type, mimeType);
722         this._statusCode = statusCode;
723         this._statusText = statusText;
724         this._responseHeaders = responseHeaders || {};
725         this._responseCookies = null;
726         this._responseReceivedTimestamp = elapsedTime || NaN;
727         this._timingData = WI.ResourceTimingData.fromPayload(timingData, this);
728
729         if (source)
730             this._responseSource = WI.Resource.responseSourceFromPayload(source);
731
732         const headerBaseSize = 12; // Length of "HTTP/1.1 ", " ", and "\r\n".
733         const headerPad = 4; // Length of ": " and "\r\n".
734         this._estimatedResponseHeadersSize = String(this._statusCode).length + this._statusText.length + headerBaseSize;
735         for (let name in this._responseHeaders)
736             this._estimatedResponseHeadersSize += name.length + this._responseHeaders[name].length + headerPad;
737
738         if (!this._cached) {
739             if (statusCode === 304 || (this._responseSource === WI.Resource.ResponseSource.MemoryCache || this._responseSource === WI.Resource.ResponseSource.DiskCache))
740                 this.markAsCached();
741         }
742
743         if (oldURL !== url) {
744             // Delete the URL components so the URL is re-parsed the next time it is requested.
745             this._urlComponents = null;
746
747             this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
748         }
749
750         if (oldMIMEType !== mimeType) {
751             // Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested.
752             this._mimeTypeComponents = null;
753
754             this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType});
755         }
756
757         if (oldType !== type)
758             this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
759
760         console.assert(isNaN(this._estimatedSize));
761         console.assert(isNaN(this._estimatedTransferSize));
762
763         // The transferSize becomes 0 when status is 304 or Content-Length is available, so
764         // notify listeners of that change.
765         if (statusCode === 304 || this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
766             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
767
768         this.dispatchEventToListeners(WI.Resource.Event.ResponseReceived);
769         this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
770     }
771
772     updateWithMetrics(metrics)
773     {
774         this._receivedNetworkLoadMetrics = true;
775
776         if (metrics.protocol)
777             this._protocol = metrics.protocol;
778         if (metrics.priority)
779             this._priority = WI.Resource.networkPriorityFromPayload(metrics.priority);
780         if (metrics.remoteAddress)
781             this._remoteAddress = metrics.remoteAddress;
782         if (metrics.connectionIdentifier)
783             this._connectionIdentifier = WI.Resource.connectionIdentifierFromPayload(metrics.connectionIdentifier);
784         if (metrics.requestHeaders) {
785             this._requestHeaders = metrics.requestHeaders;
786             this._requestCookies = null;
787             this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
788         }
789
790         if ("requestHeaderBytesSent" in metrics) {
791             this._requestHeadersTransferSize = metrics.requestHeaderBytesSent;
792             this._requestBodyTransferSize = metrics.requestBodyBytesSent;
793             this._responseHeadersTransferSize = metrics.responseHeaderBytesReceived;
794             this._responseBodyTransferSize = metrics.responseBodyBytesReceived;
795             this._responseBodySize = metrics.responseBodyDecodedSize;
796
797             console.assert(this._requestHeadersTransferSize >= 0);
798             console.assert(this._requestBodyTransferSize >= 0);
799             console.assert(this._responseHeadersTransferSize >= 0);
800             console.assert(this._responseBodyTransferSize >= 0);
801             console.assert(this._responseBodySize >= 0);
802
803             this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize: this._estimatedSize});
804             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
805         }
806
807         this.dispatchEventToListeners(WI.Resource.Event.MetricsDidChange);
808     }
809
810     setCachedResponseBodySize(size)
811     {
812         console.assert(!isNaN(size), "Size should be a valid number.");
813         console.assert(isNaN(this._cachedResponseBodySize), "This should only be set once.");
814         console.assert(this._estimatedSize === size, "The legacy path was updated already and matches.");
815
816         this._cachedResponseBodySize = size;
817     }
818
819     requestContentFromBackend()
820     {
821         // If we have the requestIdentifier we can get the actual response for this specific resource.
822         // Otherwise the content will be cached resource data, which might not exist anymore.
823         if (this._requestIdentifier)
824             return NetworkAgent.getResponseBody(this._requestIdentifier);
825
826         // There is no request identifier or frame to request content from.
827         if (this._parentFrame)
828             return PageAgent.getResourceContent(this._parentFrame.id, this._url);
829
830         return Promise.reject(new Error("Content request failed."));
831     }
832
833     increaseSize(dataLength, elapsedTime)
834     {
835         console.assert(dataLength >= 0);
836         console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated size.");
837
838         if (isNaN(this._estimatedSize))
839             this._estimatedSize = 0;
840
841         let previousSize = this._estimatedSize;
842
843         this._estimatedSize += dataLength;
844
845         this._lastDataReceivedTimestamp = elapsedTime || NaN;
846
847         this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize});
848
849         // The estimatedTransferSize is based off of size when status is not 304 or Content-Length is missing.
850         if (isNaN(this._estimatedTransferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
851             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
852     }
853
854     increaseTransferSize(encodedDataLength)
855     {
856         console.assert(encodedDataLength >= 0);
857         console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated transfer size.");
858
859         if (isNaN(this._estimatedTransferSize))
860             this._estimatedTransferSize = 0;
861         this._estimatedTransferSize += encodedDataLength;
862
863         this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
864     }
865
866     markAsCached()
867     {
868         this._cached = true;
869
870         this.dispatchEventToListeners(WI.Resource.Event.CacheStatusDidChange);
871
872         // The transferSize starts returning 0 when cached is true, unless status is 304.
873         if (this._statusCode !== 304)
874             this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
875     }
876
877     markAsFinished(elapsedTime)
878     {
879         console.assert(!this._failed);
880         console.assert(!this._canceled);
881
882         this._finished = true;
883         this._finishedOrFailedTimestamp = elapsedTime || NaN;
884         this._timingData.markResponseEndTime(elapsedTime || NaN);
885
886         if (this._finishThenRequestContentPromise)
887             this._finishThenRequestContentPromise = null;
888
889         this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFinish);
890         this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
891     }
892
893     markAsFailed(canceled, elapsedTime, errorText)
894     {
895         console.assert(!this._finished);
896
897         this._failed = true;
898         this._canceled = canceled;
899         this._finishedOrFailedTimestamp = elapsedTime || NaN;
900
901         if (!this._failureReasonText)
902             this._failureReasonText = errorText || null;
903
904         this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFail);
905         this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
906     }
907
908     revertMarkAsFinished()
909     {
910         console.assert(!this._failed);
911         console.assert(!this._canceled);
912         console.assert(this._finished);
913
914         this._finished = false;
915         this._finishedOrFailedTimestamp = NaN;
916     }
917
918     legacyMarkServedFromMemoryCache()
919     {
920         // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the MemoryCache.
921         console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown);
922
923         this._responseSource = WI.Resource.ResponseSource.MemoryCache;
924
925         this.markAsCached();
926     }
927
928     legacyMarkServedFromDiskCache()
929     {
930         // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the DiskCache.
931         console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown);
932
933         this._responseSource = WI.Resource.ResponseSource.DiskCache;
934
935         this.markAsCached();
936     }
937
938     isLoading()
939     {
940         return !this._finished && !this._failed;
941     }
942
943     hadLoadingError()
944     {
945         return this._failed || this._canceled || this._statusCode >= 400;
946     }
947
948     getImageSize(callback)
949     {
950         // Throw an error in the case this resource is not an image.
951         if (this.type !== WI.Resource.Type.Image)
952             throw "Resource is not an image.";
953
954         // See if we've already computed and cached the image size,
955         // in which case we can provide them directly.
956         if (this._imageSize !== undefined) {
957             callback(this._imageSize);
958             return;
959         }
960
961         var objectURL = null;
962
963         // Event handler for the image "load" event.
964         function imageDidLoad() {
965             URL.revokeObjectURL(objectURL);
966
967             // Cache the image metrics.
968             this._imageSize = {
969                 width: image.width,
970                 height: image.height
971             };
972
973             callback(this._imageSize);
974         }
975
976         function requestContentFailure() {
977             this._imageSize = null;
978             callback(this._imageSize);
979         }
980
981         // Create an <img> element that we'll use to load the image resource
982         // so that we can query its intrinsic size.
983         var image = new Image;
984         image.addEventListener("load", imageDidLoad.bind(this), false);
985
986         // Set the image source using an object URL once we've obtained its data.
987         this.requestContent().then((content) => {
988             objectURL = image.src = content.sourceCode.createObjectURL();
989             if (!objectURL)
990                 requestContentFailure.call(this);
991         }, requestContentFailure.bind(this));
992     }
993
994     requestContent()
995     {
996         if (this._finished)
997             return super.requestContent();
998
999         if (this._failed)
1000             return Promise.resolve({error: WI.UIString("An error occurred trying to load the resource.")});
1001
1002         if (!this._finishThenRequestContentPromise) {
1003             this._finishThenRequestContentPromise = new Promise((resolve, reject) => {
1004                 this.addEventListener(WI.Resource.Event.LoadingDidFinish, resolve);
1005                 this.addEventListener(WI.Resource.Event.LoadingDidFail, reject);
1006             }).then(WI.SourceCode.prototype.requestContent.bind(this));
1007         }
1008
1009         return this._finishThenRequestContentPromise;
1010     }
1011
1012     associateWithScript(script)
1013     {
1014         if (!this._scripts)
1015             this._scripts = [];
1016
1017         this._scripts.push(script);
1018
1019         if (this._type === WI.Resource.Type.Other || this._type === WI.Resource.Type.XHR) {
1020             let oldType = this._type;
1021             this._type = WI.Resource.Type.Script;
1022             this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
1023         }
1024     }
1025
1026     saveIdentityToCookie(cookie)
1027     {
1028         cookie[WI.Resource.URLCookieKey] = this.url.hash;
1029         cookie[WI.Resource.MainResourceCookieKey] = this.isMainResource();
1030     }
1031
1032     generateCURLCommand()
1033     {
1034         function escapeStringPosix(str) {
1035             function escapeCharacter(x) {
1036                 let code = x.charCodeAt(0);
1037                 let hex = code.toString(16);
1038                 if (code < 256)
1039                     return "\\x" + hex.padStart(2, "0");
1040                 return "\\u" + hex.padStart(4, "0");
1041             }
1042
1043             if (/[^\x20-\x7E]|'/.test(str)) {
1044                 // Use ANSI-C quoting syntax.
1045                 return "$'" + str.replace(/\\/g, "\\\\")
1046                                  .replace(/'/g, "\\'")
1047                                  .replace(/\n/g, "\\n")
1048                                  .replace(/\r/g, "\\r")
1049                                  .replace(/!/g, "\\041")
1050                                  .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
1051             } else {
1052                 // Use single quote syntax.
1053                 return `'${str}'`;
1054             }
1055         }
1056
1057         let command = ["curl " + escapeStringPosix(this.url).replace(/[[{}\]]/g, "\\$&")];
1058         command.push(`-X${this.requestMethod}`);
1059
1060         for (let key in this.requestHeaders)
1061             command.push("-H " + escapeStringPosix(`${key}: ${this.requestHeaders[key]}`));
1062
1063         if (this.requestDataContentType && this.requestMethod !== "GET" && this.requestData) {
1064             if (this.requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
1065                 command.push("--data " + escapeStringPosix(this.requestData));
1066             else
1067                 command.push("--data-binary " + escapeStringPosix(this.requestData));
1068         }
1069
1070         let curlCommand = command.join(" \\\n");
1071         InspectorFrontendHost.copyText(curlCommand);
1072         return curlCommand;
1073     }
1074 };
1075
1076 WI.Resource.TypeIdentifier = "resource";
1077 WI.Resource.URLCookieKey = "resource-url";
1078 WI.Resource.MainResourceCookieKey = "resource-is-main-resource";
1079
1080 WI.Resource.Event = {
1081     URLDidChange: "resource-url-did-change",
1082     MIMETypeDidChange: "resource-mime-type-did-change",
1083     TypeDidChange: "resource-type-did-change",
1084     RequestHeadersDidChange: "resource-request-headers-did-change",
1085     ResponseReceived: "resource-response-received",
1086     LoadingDidFinish: "resource-loading-did-finish",
1087     LoadingDidFail: "resource-loading-did-fail",
1088     TimestampsDidChange: "resource-timestamps-did-change",
1089     SizeDidChange: "resource-size-did-change",
1090     TransferSizeDidChange: "resource-transfer-size-did-change",
1091     CacheStatusDidChange: "resource-cached-did-change",
1092     MetricsDidChange: "resource-metrics-did-change",
1093     InitiatedResourcesDidChange: "resource-initiated-resources-did-change",
1094 };
1095
1096 // Keep these in sync with the "ResourceType" enum defined by the "Page" domain.
1097 WI.Resource.Type = {
1098     Document: "resource-type-document",
1099     Stylesheet: "resource-type-stylesheet",
1100     Image: "resource-type-image",
1101     Font: "resource-type-font",
1102     Script: "resource-type-script",
1103     XHR: "resource-type-xhr",
1104     Fetch: "resource-type-fetch",
1105     Ping: "resource-type-ping",
1106     Beacon: "resource-type-beacon",
1107     WebSocket: "resource-type-websocket",
1108     Other: "resource-type-other",
1109 };
1110
1111 WI.Resource.ResponseSource = {
1112     Unknown: Symbol("unknown"),
1113     Network: Symbol("network"),
1114     MemoryCache: Symbol("memory-cache"),
1115     DiskCache: Symbol("disk-cache"),
1116     ServiceWorker: Symbol("service-worker"),
1117 };
1118
1119 WI.Resource.NetworkPriority = {
1120     Unknown: Symbol("unknown"),
1121     Low: Symbol("low"),
1122     Medium: Symbol("medium"),
1123     High: Symbol("high"),
1124 };
1125
1126 // This MIME Type map is private, use WI.Resource.typeFromMIMEType().
1127 WI.Resource._mimeTypeMap = {
1128     "text/html": WI.Resource.Type.Document,
1129     "text/xml": WI.Resource.Type.Document,
1130     "text/plain": WI.Resource.Type.Document,
1131     "application/xhtml+xml": WI.Resource.Type.Document,
1132     "image/svg+xml": WI.Resource.Type.Document,
1133
1134     "text/css": WI.Resource.Type.Stylesheet,
1135     "text/xsl": WI.Resource.Type.Stylesheet,
1136     "text/x-less": WI.Resource.Type.Stylesheet,
1137     "text/x-sass": WI.Resource.Type.Stylesheet,
1138     "text/x-scss": WI.Resource.Type.Stylesheet,
1139
1140     "application/pdf": WI.Resource.Type.Image,
1141
1142     "application/x-font-type1": WI.Resource.Type.Font,
1143     "application/x-font-ttf": WI.Resource.Type.Font,
1144     "application/x-font-woff": WI.Resource.Type.Font,
1145     "application/x-truetype-font": WI.Resource.Type.Font,
1146
1147     "text/javascript": WI.Resource.Type.Script,
1148     "text/ecmascript": WI.Resource.Type.Script,
1149     "application/javascript": WI.Resource.Type.Script,
1150     "application/ecmascript": WI.Resource.Type.Script,
1151     "application/x-javascript": WI.Resource.Type.Script,
1152     "application/json": WI.Resource.Type.Script,
1153     "application/x-json": WI.Resource.Type.Script,
1154     "text/x-javascript": WI.Resource.Type.Script,
1155     "text/x-json": WI.Resource.Type.Script,
1156     "text/javascript1.1": WI.Resource.Type.Script,
1157     "text/javascript1.2": WI.Resource.Type.Script,
1158     "text/javascript1.3": WI.Resource.Type.Script,
1159     "text/jscript": WI.Resource.Type.Script,
1160     "text/livescript": WI.Resource.Type.Script,
1161     "text/x-livescript": WI.Resource.Type.Script,
1162     "text/typescript": WI.Resource.Type.Script,
1163     "text/x-clojure": WI.Resource.Type.Script,
1164     "text/x-coffeescript": WI.Resource.Type.Script
1165 };