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