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