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