Web Inspector: Include more Network information in Resource Details Sidebar
[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 WebInspector.Resource = class Resource extends WebInspector.SourceCode
28 {
29     constructor(url, mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp)
30     {
31         super();
32
33         console.assert(url);
34
35         if (type in WebInspector.Resource.Type)
36             type = WebInspector.Resource.Type[type];
37
38         this._url = url;
39         this._urlComponents = null;
40         this._mimeType = mimeType;
41         this._mimeTypeComponents = null;
42         this._type = type || WebInspector.Resource.typeFromMIMEType(mimeType);
43         this._loaderIdentifier = loaderIdentifier || null;
44         this._requestIdentifier = requestIdentifier || null;
45         this._requestMethod = requestMethod || null;
46         this._requestData = requestData || null;
47         this._requestHeaders = requestHeaders || {};
48         this._responseHeaders = {};
49         this._parentFrame = null;
50         this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null;
51         this._initiatedResources = [];
52         this._originalRequestWillBeSentTimestamp = originalRequestWillBeSentTimestamp || null;
53         this._requestSentTimestamp = requestSentTimestamp || NaN;
54         this._responseReceivedTimestamp = NaN;
55         this._lastRedirectReceivedTimestamp = NaN;
56         this._lastDataReceivedTimestamp = NaN;
57         this._finishedOrFailedTimestamp = NaN;
58         this._finishThenRequestContentPromise = null;
59         this._statusCode = NaN;
60         this._statusText = null;
61         this._size = NaN;
62         this._transferSize = NaN;
63         this._cached = false;
64         this._responseSource = WebInspector.Resource.ResponseSource.Unknown;
65         this._timingData = new WebInspector.ResourceTimingData(this);
66         this._protocol = null;
67         this._priority = WebInspector.Resource.NetworkPriority.Unknown;
68         this._remoteAddress = null;
69         this._connectionIdentifier = null;
70         this._target = targetId ? WebInspector.targetManager.targetForIdentifier(targetId) : WebInspector.mainTarget;
71
72         if (this._initiatorSourceCodeLocation && this._initiatorSourceCodeLocation.sourceCode instanceof WebInspector.Resource)
73             this._initiatorSourceCodeLocation.sourceCode.addInitiatedResource(this);
74     }
75
76     // Static
77
78     static typeFromMIMEType(mimeType)
79     {
80         if (!mimeType)
81             return WebInspector.Resource.Type.Other;
82
83         mimeType = parseMIMEType(mimeType).type;
84
85         if (mimeType in WebInspector.Resource._mimeTypeMap)
86             return WebInspector.Resource._mimeTypeMap[mimeType];
87
88         if (mimeType.startsWith("image/"))
89             return WebInspector.Resource.Type.Image;
90
91         if (mimeType.startsWith("font/"))
92             return WebInspector.Resource.Type.Font;
93
94         return WebInspector.Resource.Type.Other;
95     }
96
97     static displayNameForType(type, plural)
98     {
99         switch (type) {
100         case WebInspector.Resource.Type.Document:
101             if (plural)
102                 return WebInspector.UIString("Documents");
103             return WebInspector.UIString("Document");
104         case WebInspector.Resource.Type.Stylesheet:
105             if (plural)
106                 return WebInspector.UIString("Stylesheets");
107             return WebInspector.UIString("Stylesheet");
108         case WebInspector.Resource.Type.Image:
109             if (plural)
110                 return WebInspector.UIString("Images");
111             return WebInspector.UIString("Image");
112         case WebInspector.Resource.Type.Font:
113             if (plural)
114                 return WebInspector.UIString("Fonts");
115             return WebInspector.UIString("Font");
116         case WebInspector.Resource.Type.Script:
117             if (plural)
118                 return WebInspector.UIString("Scripts");
119             return WebInspector.UIString("Script");
120         case WebInspector.Resource.Type.XHR:
121             if (plural)
122                 return WebInspector.UIString("XHRs");
123             return WebInspector.UIString("XHR");
124         case WebInspector.Resource.Type.Fetch:
125             if (plural)
126                 return WebInspector.UIString("Fetches");
127             return WebInspector.UIString("Fetch");
128         case WebInspector.Resource.Type.WebSocket:
129             if (plural)
130                 return WebInspector.UIString("Sockets");
131             return WebInspector.UIString("Socket");
132         case WebInspector.Resource.Type.Other:
133             return WebInspector.UIString("Other");
134         default:
135             console.error("Unknown resource type", type);
136             return null;
137         }
138     }
139
140     static displayNameForProtocol(protocol)
141     {
142         switch (protocol) {
143         case "h2":
144             return "HTTP/2";
145         case "http/1.0":
146             return "HTTP/1.0";
147         case "http/1.1":
148             return "HTTP/1.1";
149         case "spdy/2":
150             return "SPDY/2";
151         case "spdy/3":
152             return "SPDY/3";
153         case "spdy/3.1":
154             return "SPDY/3.1";
155         default:
156             return null;
157         }
158     }
159
160     static displayNameForPriority(priority)
161     {
162         switch (priority) {
163         case WebInspector.Resource.NetworkPriority.Low:
164             return WebInspector.UIString("Low");
165         case WebInspector.Resource.NetworkPriority.Medium:
166             return WebInspector.UIString("Medium");
167         case WebInspector.Resource.NetworkPriority.High:
168             return WebInspector.UIString("High");
169         default:
170             return null;
171         }
172     }
173
174     static responseSourceFromPayload(source)
175     {
176         if (!source)
177             return WebInspector.Resource.ResponseSource.Unknown;
178
179         switch (source) {
180         case NetworkAgent.ResponseSource.Unknown:
181             return WebInspector.Resource.ResponseSource.Unknown;
182         case NetworkAgent.ResponseSource.Network:
183             return WebInspector.Resource.ResponseSource.Network;
184         case NetworkAgent.ResponseSource.MemoryCache:
185             return WebInspector.Resource.ResponseSource.MemoryCache;
186         case NetworkAgent.ResponseSource.DiskCache:
187             return WebInspector.Resource.ResponseSource.DiskCache;
188         default:
189             console.error("Unknown response source type", source);
190             return WebInspector.Resource.ResponseSource.Unknown;
191         }
192     }
193
194     static networkPriorityFromPayload(priority)
195     {
196         switch (priority) {
197         case NetworkAgent.MetricsPriority.Low:
198             return WebInspector.Resource.NetworkPriority.Low;
199         case NetworkAgent.MetricsPriority.Medium:
200             return WebInspector.Resource.NetworkPriority.Medium;
201         case NetworkAgent.MetricsPriority.High:
202             return WebInspector.Resource.NetworkPriority.High;
203         default:
204             console.error("Unknown metrics priority", priority);
205             return WebInspector.Resource.NetworkPriority.Unknown;
206         }
207     }
208
209     static connectionIdentifierFromPayload(connectionIdentifier)
210     {
211         // Map backend connection identifiers to an easier to read number.
212         if (!WebInspector.Resource.connectionIdentifierMap) {
213             WebInspector.Resource.connectionIdentifierMap = new Map;
214             WebInspector.Resource.nextConnectionIdentifier = 1;
215         }
216
217         let id = WebInspector.Resource.connectionIdentifierMap.get(connectionIdentifier);
218         if (id)
219             return id;
220
221         id = WebInspector.Resource.nextConnectionIdentifier++;
222         WebInspector.Resource.connectionIdentifierMap.set(connectionIdentifier, id);
223         return id;
224     }
225
226     // Public
227
228     get target() { return this._target; }
229     get type() { return this._type; }
230     get loaderIdentifier() { return this._loaderIdentifier; }
231     get requestIdentifier() { return this._requestIdentifier; }
232     get requestMethod() { return this._requestMethod; }
233     get requestData() { return this._requestData; }
234     get statusCode() { return this._statusCode; }
235     get statusText() { return this._statusText; }
236     get responseSource() { return this._responseSource; }
237     get timingData() { return this._timingData; }
238     get protocol() { return this._protocol; }
239     get priority() { return this._priority; }
240     get remoteAddress() { return this._remoteAddress; }
241     get connectionIdentifier() { return this._connectionIdentifier; }
242
243     get url()
244     {
245         return this._url;
246     }
247
248     get urlComponents()
249     {
250         if (!this._urlComponents)
251             this._urlComponents = parseURL(this._url);
252         return this._urlComponents;
253     }
254
255     get displayName()
256     {
257         return WebInspector.displayNameForURL(this._url, this.urlComponents);
258     }
259
260     get displayURL()
261     {
262         const isMultiLine = true;
263         const dataURIMaxSize = 64;
264         return WebInspector.truncateURL(this._url, isMultiLine, dataURIMaxSize);
265     }
266
267     get initiatorSourceCodeLocation()
268     {
269         return this._initiatorSourceCodeLocation;
270     }
271
272     get initiatedResources()
273     {
274         return this._initiatedResources;
275     }
276
277     get originalRequestWillBeSentTimestamp()
278     {
279         return this._originalRequestWillBeSentTimestamp;
280     }
281
282     get mimeType()
283     {
284         return this._mimeType;
285     }
286
287     get mimeTypeComponents()
288     {
289         if (!this._mimeTypeComponents)
290             this._mimeTypeComponents = parseMIMEType(this._mimeType);
291         return this._mimeTypeComponents;
292     }
293
294     get syntheticMIMEType()
295     {
296         // Resources are often transferred with a MIME-type that doesn't match the purpose the
297         // resource was loaded for, which is what WebInspector.Resource.Type represents.
298         // This getter generates a MIME-type, if needed, that matches the resource type.
299
300         // If the type matches the Resource.Type of the MIME-type, then return the actual MIME-type.
301         if (this._type === WebInspector.Resource.typeFromMIMEType(this._mimeType))
302             return this._mimeType;
303
304         // Return the default MIME-types for the Resource.Type, since the current MIME-type
305         // does not match what is expected for the Resource.Type.
306         switch (this._type) {
307         case WebInspector.Resource.Type.Stylesheet:
308             return "text/css";
309         case WebInspector.Resource.Type.Script:
310             return "text/javascript";
311         }
312
313         // Return the actual MIME-type since we don't have a better synthesized one to return.
314         return this._mimeType;
315     }
316
317     createObjectURL()
318     {
319         // If content is not available, fallback to using original URL.
320         // The client may try to revoke it, but nothing will happen.
321         if (!this.content)
322             return this._url;
323
324         var content = this.content;
325         console.assert(content instanceof Blob, content);
326
327         return URL.createObjectURL(content);
328     }
329
330     isMainResource()
331     {
332         return this._parentFrame ? this._parentFrame.mainResource === this : false;
333     }
334
335     addInitiatedResource(resource)
336     {
337         if (!(resource instanceof WebInspector.Resource))
338             return;
339
340         this._initiatedResources.push(resource);
341
342         this.dispatchEventToListeners(WebInspector.Resource.Event.InitiatedResourcesDidChange);
343     }
344
345     get parentFrame()
346     {
347         return this._parentFrame;
348     }
349
350     get finished()
351     {
352         return this._finished;
353     }
354
355     get failed()
356     {
357         return this._failed;
358     }
359
360     get canceled()
361     {
362         return this._canceled;
363     }
364
365     get requestDataContentType()
366     {
367         return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null;
368     }
369
370     get requestHeaders()
371     {
372         return this._requestHeaders;
373     }
374
375     get responseHeaders()
376     {
377         return this._responseHeaders;
378     }
379
380     get requestSentTimestamp()
381     {
382         return this._requestSentTimestamp;
383     }
384
385     get lastRedirectReceivedTimestamp()
386     {
387         return this._lastRedirectReceivedTimestamp;
388     }
389
390     get responseReceivedTimestamp()
391     {
392         return this._responseReceivedTimestamp;
393     }
394
395     get lastDataReceivedTimestamp()
396     {
397         return this._lastDataReceivedTimestamp;
398     }
399
400     get finishedOrFailedTimestamp()
401     {
402         return this._finishedOrFailedTimestamp;
403     }
404
405     get firstTimestamp()
406     {
407         return this.timingData.startTime || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp;
408     }
409
410     get lastTimestamp()
411     {
412         return this.timingData.responseEnd || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp;
413     }
414
415     get duration()
416     {
417         return this.timingData.responseEnd - this.timingData.requestStart;
418     }
419
420     get latency()
421     {
422         return this.timingData.responseStart - this.timingData.requestStart;
423     }
424
425     get receiveDuration()
426     {
427         return this.timingData.responseEnd - this.timingData.responseStart;
428     }
429
430     get cached()
431     {
432         return this._cached;
433     }
434
435     get size()
436     {
437         return this._size;
438     }
439
440     get encodedSize()
441     {
442         if (!isNaN(this._transferSize))
443             return this._transferSize;
444
445         // If we did not receive actual transfer size from network
446         // stack, we prefer using Content-Length over resourceSize as
447         // resourceSize may differ from actual transfer size if platform's
448         // network stack performed decoding (e.g. gzip decompression).
449         // The Content-Length, though, is expected to come from raw
450         // response headers and will reflect actual transfer length.
451         // This won't work for chunked content encoding, so fall back to
452         // resourceSize when we don't have Content-Length. This still won't
453         // work for chunks with non-trivial encodings. We need a way to
454         // get actual transfer size from the network stack.
455
456         return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._size);
457     }
458
459     get transferSize()
460     {
461         if (this.statusCode === 304) // Not modified
462             return this._responseHeadersSize;
463
464         if (this._cached)
465             return 0;
466
467         return this._responseHeadersSize + this.encodedSize;
468     }
469
470     get compressed()
471     {
472         var contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
473         return contentEncoding && /\b(?:gzip|deflate)\b/.test(contentEncoding);
474     }
475
476     get scripts()
477     {
478         return this._scripts || [];
479     }
480
481     scriptForLocation(sourceCodeLocation)
482     {
483         console.assert(!(this instanceof WebInspector.SourceMapResource));
484         console.assert(sourceCodeLocation.sourceCode === this, "SourceCodeLocation must be in this Resource");
485         if (sourceCodeLocation.sourceCode !== this)
486             return null;
487
488         var lineNumber = sourceCodeLocation.lineNumber;
489         var columnNumber = sourceCodeLocation.columnNumber;
490         for (var i = 0; i < this._scripts.length; ++i) {
491             var script = this._scripts[i];
492             if (script.range.startLine <= lineNumber && script.range.endLine >= lineNumber) {
493                 if (script.range.startLine === lineNumber && columnNumber < script.range.startColumn)
494                     continue;
495                 if (script.range.endLine === lineNumber && columnNumber > script.range.endColumn)
496                     continue;
497                 return script;
498             }
499         }
500
501         return null;
502     }
503
504     updateForRedirectResponse(url, requestHeaders, elapsedTime)
505     {
506         console.assert(!this._finished);
507         console.assert(!this._failed);
508         console.assert(!this._canceled);
509
510         var oldURL = this._url;
511
512         this._url = url;
513         this._requestHeaders = requestHeaders || {};
514         this._lastRedirectReceivedTimestamp = elapsedTime || NaN;
515
516         if (oldURL !== url) {
517             // Delete the URL components so the URL is re-parsed the next time it is requested.
518             this._urlComponents = null;
519
520             this.dispatchEventToListeners(WebInspector.Resource.Event.URLDidChange, {oldURL});
521         }
522
523         this.dispatchEventToListeners(WebInspector.Resource.Event.RequestHeadersDidChange);
524         this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
525     }
526
527     hasResponse()
528     {
529         return !isNaN(this._statusCode);
530     }
531
532     updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, elapsedTime, timingData, source)
533     {
534         console.assert(!this._finished);
535         console.assert(!this._failed);
536         console.assert(!this._canceled);
537
538         let oldURL = this._url;
539         let oldMIMEType = this._mimeType;
540         let oldType = this._type;
541
542         if (type in WebInspector.Resource.Type)
543             type = WebInspector.Resource.Type[type];
544
545         this._url = url;
546         this._mimeType = mimeType;
547         this._type = type || WebInspector.Resource.typeFromMIMEType(mimeType);
548         this._statusCode = statusCode;
549         this._statusText = statusText;
550         this._responseHeaders = responseHeaders || {};
551         this._responseReceivedTimestamp = elapsedTime || NaN;
552         this._timingData = WebInspector.ResourceTimingData.fromPayload(timingData, this);
553
554         if (source)
555             this._responseSource = WebInspector.Resource.responseSourceFromPayload(source);
556
557         this._responseHeadersSize = String(this._statusCode).length + this._statusText.length + 12; // Extra length is for "HTTP/1.1 ", " ", and "\r\n".
558         for (let name in this._responseHeaders)
559             this._responseHeadersSize += name.length + this._responseHeaders[name].length + 4; // Extra length is for ": ", and "\r\n".
560
561         if (!this._cached) {
562             if (statusCode === 304 || (this._responseSource === WebInspector.Resource.ResponseSource.MemoryCache || this._responseSource === WebInspector.Resource.ResponseSource.DiskCache))
563                 this.markAsCached();
564         }
565
566         if (oldURL !== url) {
567             // Delete the URL components so the URL is re-parsed the next time it is requested.
568             this._urlComponents = null;
569
570             this.dispatchEventToListeners(WebInspector.Resource.Event.URLDidChange, {oldURL});
571         }
572
573         if (oldMIMEType !== mimeType) {
574             // Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested.
575             this._mimeTypeComponents = null;
576
577             this.dispatchEventToListeners(WebInspector.Resource.Event.MIMETypeDidChange, {oldMIMEType});
578         }
579
580         if (oldType !== type)
581             this.dispatchEventToListeners(WebInspector.Resource.Event.TypeDidChange, {oldType});
582
583         console.assert(isNaN(this._size));
584         console.assert(isNaN(this._transferSize));
585
586         // The transferSize becomes 0 when status is 304 or Content-Length is available, so
587         // notify listeners of that change.
588         if (statusCode === 304 || this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
589             this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
590
591         this.dispatchEventToListeners(WebInspector.Resource.Event.ResponseReceived);
592         this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
593     }
594
595     updateWithMetrics(metrics)
596     {
597         if (metrics.protocol)
598             this._protocol = metrics.protocol;
599         if (metrics.priority)
600             this._priority = WebInspector.Resource.networkPriorityFromPayload(metrics.priority);
601         if (metrics.remoteAddress)
602             this._remoteAddress = metrics.remoteAddress;
603         if (metrics.connectionIdentifier)
604             this._connectionIdentifier = WebInspector.Resource.connectionIdentifierFromPayload(metrics.connectionIdentifier);
605     }
606
607     requestContentFromBackend()
608     {
609         // If we have the requestIdentifier we can get the actual response for this specific resource.
610         // Otherwise the content will be cached resource data, which might not exist anymore.
611         if (this._requestIdentifier)
612             return NetworkAgent.getResponseBody(this._requestIdentifier);
613
614         // There is no request identifier or frame to request content from.
615         if (this._parentFrame)
616             return PageAgent.getResourceContent(this._parentFrame.id, this._url);
617
618         return Promise.reject(new Error("Content request failed."));
619     }
620
621     increaseSize(dataLength, elapsedTime)
622     {
623         console.assert(dataLength >= 0);
624
625         if (isNaN(this._size))
626             this._size = 0;
627
628         var previousSize = this._size;
629
630         this._size += dataLength;
631
632         this._lastDataReceivedTimestamp = elapsedTime || NaN;
633
634         this.dispatchEventToListeners(WebInspector.Resource.Event.SizeDidChange, {previousSize});
635
636         // The transferSize is based off of size when status is not 304 or Content-Length is missing.
637         if (isNaN(this._transferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
638             this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
639     }
640
641     increaseTransferSize(encodedDataLength)
642     {
643         console.assert(encodedDataLength >= 0);
644
645         if (isNaN(this._transferSize))
646             this._transferSize = 0;
647         this._transferSize += encodedDataLength;
648
649         this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
650     }
651
652     markAsCached()
653     {
654         this._cached = true;
655
656         this.dispatchEventToListeners(WebInspector.Resource.Event.CacheStatusDidChange);
657
658         // The transferSize starts returning 0 when cached is true, unless status is 304.
659         if (this._statusCode !== 304)
660             this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
661     }
662
663     markAsFinished(elapsedTime)
664     {
665         console.assert(!this._failed);
666         console.assert(!this._canceled);
667
668         this._finished = true;
669         this._finishedOrFailedTimestamp = elapsedTime || NaN;
670         this._timingData.markResponseEndTime(elapsedTime || NaN);
671
672         if (this._finishThenRequestContentPromise)
673             this._finishThenRequestContentPromise = null;
674
675         this.dispatchEventToListeners(WebInspector.Resource.Event.LoadingDidFinish);
676         this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
677     }
678
679     markAsFailed(canceled, elapsedTime)
680     {
681         console.assert(!this._finished);
682
683         this._failed = true;
684         this._canceled = canceled;
685         this._finishedOrFailedTimestamp = elapsedTime || NaN;
686
687         this.dispatchEventToListeners(WebInspector.Resource.Event.LoadingDidFail);
688         this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
689     }
690
691     revertMarkAsFinished()
692     {
693         console.assert(!this._failed);
694         console.assert(!this._canceled);
695         console.assert(this._finished);
696
697         this._finished = false;
698         this._finishedOrFailedTimestamp = NaN;
699     }
700
701     legacyMarkServedFromMemoryCache()
702     {
703         // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the MemoryCache.
704         console.assert(this._responseSource === WebInspector.Resource.ResponseSource.Unknown);
705
706         this._responseSource = WebInspector.Resource.ResponseSource.MemoryCache;
707
708         this.markAsCached();
709     }
710
711     legacyMarkServedFromDiskCache()
712     {
713         // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the DiskCache.
714         console.assert(this._responseSource === WebInspector.Resource.ResponseSource.Unknown);
715
716         this._responseSource = WebInspector.Resource.ResponseSource.DiskCache;
717
718         this.markAsCached();
719     }
720
721     getImageSize(callback)
722     {
723         // Throw an error in the case this resource is not an image.
724         if (this.type !== WebInspector.Resource.Type.Image)
725             throw "Resource is not an image.";
726
727         // See if we've already computed and cached the image size,
728         // in which case we can provide them directly.
729         if (this._imageSize !== undefined) {
730             callback(this._imageSize);
731             return;
732         }
733
734         var objectURL = null;
735
736         // Event handler for the image "load" event.
737         function imageDidLoad() {
738             URL.revokeObjectURL(objectURL);
739
740             // Cache the image metrics.
741             this._imageSize = {
742                 width: image.width,
743                 height: image.height
744             };
745
746             callback(this._imageSize);
747         }
748
749         function requestContentFailure() {
750             this._imageSize = null;
751             callback(this._imageSize);
752         }
753
754         // Create an <img> element that we'll use to load the image resource
755         // so that we can query its intrinsic size.
756         var image = new Image;
757         image.addEventListener("load", imageDidLoad.bind(this), false);
758
759         // Set the image source using an object URL once we've obtained its data.
760         this.requestContent().then(function(content) {
761             objectURL = image.src = content.sourceCode.createObjectURL();
762         }, requestContentFailure.bind(this));
763     }
764
765     requestContent()
766     {
767         if (this._finished)
768             return super.requestContent();
769
770         if (this._failed)
771             return Promise.resolve({error: WebInspector.UIString("An error occurred trying to load the resource.")});
772
773         if (!this._finishThenRequestContentPromise) {
774             this._finishThenRequestContentPromise = new Promise((resolve, reject) => {
775                 this.addEventListener(WebInspector.Resource.Event.LoadingDidFinish, resolve);
776                 this.addEventListener(WebInspector.Resource.Event.LoadingDidFail, reject);
777             }).then(WebInspector.SourceCode.prototype.requestContent.bind(this));
778         }
779
780         return this._finishThenRequestContentPromise;
781     }
782
783     associateWithScript(script)
784     {
785         if (!this._scripts)
786             this._scripts = [];
787
788         this._scripts.push(script);
789
790         if (this._type === WebInspector.Resource.Type.Other || this._type === WebInspector.Resource.Type.XHR) {
791             let oldType = this._type;
792             this._type = WebInspector.Resource.Type.Script;
793             this.dispatchEventToListeners(WebInspector.Resource.Event.TypeDidChange, {oldType});
794         }
795     }
796
797     saveIdentityToCookie(cookie)
798     {
799         cookie[WebInspector.Resource.URLCookieKey] = this.url.hash;
800         cookie[WebInspector.Resource.MainResourceCookieKey] = this.isMainResource();
801     }
802
803     generateCURLCommand()
804     {
805         function escapeStringPosix(str) {
806             function escapeCharacter(x) {
807                 let code = x.charCodeAt(0);
808                 let hex = code.toString(16);
809                 if (code < 256)
810                     return "\\x" + hex.padStart(2, "0");
811                 return "\\u" + hex.padStart(4, "0");
812             }
813
814             if (/[^\x20-\x7E]|'/.test(str)) {
815                 // Use ANSI-C quoting syntax.
816                 return "$'" + str.replace(/\\/g, "\\\\")
817                                  .replace(/'/g, "\\'")
818                                  .replace(/\n/g, "\\n")
819                                  .replace(/\r/g, "\\r")
820                                  .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
821             } else {
822                 // Use single quote syntax.
823                 return `'${str}'`;
824             }
825         }
826
827         let command = ["curl " + escapeStringPosix(this.url).replace(/[[{}\]]/g, "\\$&")];
828         command.push(`-X${this.requestMethod}`);
829
830         for (let key in this.requestHeaders)
831             command.push("-H " + escapeStringPosix(`${key}: ${this.requestHeaders[key]}`));
832
833         if (this.requestDataContentType && this.requestMethod !== "GET" && this.requestData) {
834             if (this.requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
835                 command.push("--data " + escapeStringPosix(this.requestData));
836             else
837                 command.push("--data-binary " + escapeStringPosix(this.requestData));
838         }
839
840         let curlCommand = command.join(" \\\n");
841         InspectorFrontendHost.copyText(curlCommand);
842         return curlCommand;
843     }
844 };
845
846 WebInspector.Resource.TypeIdentifier = "resource";
847 WebInspector.Resource.URLCookieKey = "resource-url";
848 WebInspector.Resource.MainResourceCookieKey = "resource-is-main-resource";
849
850 WebInspector.Resource.Event = {
851     URLDidChange: "resource-url-did-change",
852     MIMETypeDidChange: "resource-mime-type-did-change",
853     TypeDidChange: "resource-type-did-change",
854     RequestHeadersDidChange: "resource-request-headers-did-change",
855     ResponseReceived: "resource-response-received",
856     LoadingDidFinish: "resource-loading-did-finish",
857     LoadingDidFail: "resource-loading-did-fail",
858     TimestampsDidChange: "resource-timestamps-did-change",
859     SizeDidChange: "resource-size-did-change",
860     TransferSizeDidChange: "resource-transfer-size-did-change",
861     CacheStatusDidChange: "resource-cached-did-change",
862     InitiatedResourcesDidChange: "resource-initiated-resources-did-change",
863 };
864
865 // Keep these in sync with the "ResourceType" enum defined by the "Page" domain.
866 WebInspector.Resource.Type = {
867     Document: "resource-type-document",
868     Stylesheet: "resource-type-stylesheet",
869     Image: "resource-type-image",
870     Font: "resource-type-font",
871     Script: "resource-type-script",
872     XHR: "resource-type-xhr",
873     Fetch: "resource-type-fetch",
874     WebSocket: "resource-type-websocket",
875     Other: "resource-type-other"
876 };
877
878 WebInspector.Resource.ResponseSource = {
879     Unknown: Symbol("unknown"),
880     Network: Symbol("network"),
881     MemoryCache: Symbol("memory-cache"),
882     DiskCache: Symbol("disk-cache"),
883 };
884
885 WebInspector.Resource.NetworkPriority = {
886     Unknown: Symbol("unknown"),
887     Low: Symbol("low"),
888     Medium: Symbol("medium"),
889     High: Symbol("high"),
890 };
891
892 // This MIME Type map is private, use WebInspector.Resource.typeFromMIMEType().
893 WebInspector.Resource._mimeTypeMap = {
894     "text/html": WebInspector.Resource.Type.Document,
895     "text/xml": WebInspector.Resource.Type.Document,
896     "text/plain": WebInspector.Resource.Type.Document,
897     "application/xhtml+xml": WebInspector.Resource.Type.Document,
898     "image/svg+xml": WebInspector.Resource.Type.Document,
899
900     "text/css": WebInspector.Resource.Type.Stylesheet,
901     "text/xsl": WebInspector.Resource.Type.Stylesheet,
902     "text/x-less": WebInspector.Resource.Type.Stylesheet,
903     "text/x-sass": WebInspector.Resource.Type.Stylesheet,
904     "text/x-scss": WebInspector.Resource.Type.Stylesheet,
905
906     "application/pdf": WebInspector.Resource.Type.Image,
907
908     "application/x-font-type1": WebInspector.Resource.Type.Font,
909     "application/x-font-ttf": WebInspector.Resource.Type.Font,
910     "application/x-font-woff": WebInspector.Resource.Type.Font,
911     "application/x-truetype-font": WebInspector.Resource.Type.Font,
912
913     "text/javascript": WebInspector.Resource.Type.Script,
914     "text/ecmascript": WebInspector.Resource.Type.Script,
915     "application/javascript": WebInspector.Resource.Type.Script,
916     "application/ecmascript": WebInspector.Resource.Type.Script,
917     "application/x-javascript": WebInspector.Resource.Type.Script,
918     "application/json": WebInspector.Resource.Type.Script,
919     "application/x-json": WebInspector.Resource.Type.Script,
920     "text/x-javascript": WebInspector.Resource.Type.Script,
921     "text/x-json": WebInspector.Resource.Type.Script,
922     "text/javascript1.1": WebInspector.Resource.Type.Script,
923     "text/javascript1.2": WebInspector.Resource.Type.Script,
924     "text/javascript1.3": WebInspector.Resource.Type.Script,
925     "text/jscript": WebInspector.Resource.Type.Script,
926     "text/livescript": WebInspector.Resource.Type.Script,
927     "text/x-livescript": WebInspector.Resource.Type.Script,
928     "text/typescript": WebInspector.Resource.Type.Script,
929     "text/x-clojure": WebInspector.Resource.Type.Script,
930     "text/x-coffeescript": WebInspector.Resource.Type.Script
931 };