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