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