Web Inspector: Provide UIString descriptions to improve localizations
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / Resource.js
index 5b210b9..e931c12 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2011 Google Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-WebInspector.Resource = function(url, mimeType, type, loaderIdentifier, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, initiatorSourceCodeLocation)
+WI.Resource = class Resource extends WI.SourceCode
 {
-    WebInspector.SourceCode.call(this);
-
-    console.assert(url);
-
-    if (type in WebInspector.Resource.Type)
-        type = WebInspector.Resource.Type[type];
-
-    this._url = url;
-    this._mimeType = mimeType;
-    this._type = type || WebInspector.Resource.Type.fromMIMEType(mimeType);
-    this._loaderIdentifier = loaderIdentifier || null;
-    this._requestIdentifier = requestIdentifier || null;
-    this._requestMethod = requestMethod || null;
-    this._requestData = requestData || null;
-    this._requestHeaders = requestHeaders || {};
-    this._responseHeaders = {};
-    this._parentFrame = null;
-    this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null;
-    this._requestSentTimestamp = requestSentTimestamp || NaN;
-    this._responseReceivedTimestamp = NaN;
-    this._lastRedirectReceivedTimestamp = NaN;
-    this._lastDataReceivedTimestamp = NaN;
-    this._finishedOrFailedTimestamp = NaN;
-    this._size = NaN;
-    this._transferSize = NaN;
-    this._cached = false;
-};
+    constructor(url, {mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, requestSentWalltime, initiatorCallFrames, initiatorSourceCodeLocation, initiatorNode, originalRequestWillBeSentTimestamp} = {})
+    {
+        super();
 
-WebInspector.Object.addConstructorFunctions(WebInspector.Resource);
+        console.assert(url);
 
-WebInspector.Resource.TypeIdentifier = "resource";
-WebInspector.Resource.URLCookieKey = "resource-url";
-WebInspector.Resource.MainResourceCookieKey = "resource-is-main-resource";
+        if (type in WI.Resource.Type)
+            type = WI.Resource.Type[type];
 
-WebInspector.Resource.Event = {
-    URLDidChange: "resource-url-did-change",
-    MIMETypeDidChange: "resource-mime-type-did-change",
-    TypeDidChange: "resource-type-did-change",
-    RequestHeadersDidChange: "resource-request-headers-did-change",
-    ResponseReceived: "resource-response-received",
-    LoadingDidFinish: "resource-loading-did-finish",
-    LoadingDidFail: "resource-loading-did-fail",
-    TimestampsDidChange: "resource-timestamps-did-change",
-    SizeDidChange: "resource-size-did-change",
-    TransferSizeDidChange: "resource-transfer-size-did-change",
-    CacheStatusDidChange: "resource-cached-did-change"
-};
+        this._url = url;
+        this._urlComponents = null;
+        this._mimeType = mimeType;
+        this._mimeTypeComponents = null;
+        this._type = Resource.resolvedType(type, mimeType);
+        this._loaderIdentifier = loaderIdentifier || null;
+        this._requestIdentifier = requestIdentifier || null;
+        this._queryStringParameters = undefined;
+        this._requestFormParameters = undefined;
+        this._requestMethod = requestMethod || null;
+        this._requestData = requestData || null;
+        this._requestHeaders = requestHeaders || {};
+        this._responseHeaders = {};
+        this._requestCookies = null;
+        this._responseCookies = null;
+        this._serverTimingEntries = null;
+        this._parentFrame = null;
+        this._initiatorCallFrames = initiatorCallFrames || null;
+        this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null;
+        this._initiatorNode = initiatorNode || null;
+        this._initiatedResources = [];
+        this._originalRequestWillBeSentTimestamp = originalRequestWillBeSentTimestamp || null;
+        this._requestSentTimestamp = requestSentTimestamp || NaN;
+        this._requestSentWalltime = requestSentWalltime || NaN;
+        this._responseReceivedTimestamp = NaN;
+        this._lastDataReceivedTimestamp = NaN;
+        this._finishedOrFailedTimestamp = NaN;
+        this._finishThenRequestContentPromise = null;
+        this._statusCode = NaN;
+        this._statusText = null;
+        this._cached = false;
+        this._canceled = false;
+        this._finished = false;
+        this._failed = false;
+        this._failureReasonText = null;
+        this._receivedNetworkLoadMetrics = false;
+        this._responseSource = WI.Resource.ResponseSource.Unknown;
+        this._security = null;
+        this._timingData = new WI.ResourceTimingData(this);
+        this._protocol = null;
+        this._priority = WI.Resource.NetworkPriority.Unknown;
+        this._remoteAddress = null;
+        this._connectionIdentifier = null;
+        this._target = targetId ? WI.targetManager.targetForIdentifier(targetId) : WI.mainTarget;
+        this._redirects = [];
+
+        // Exact sizes if loaded over the network or cache.
+        this._requestHeadersTransferSize = NaN;
+        this._requestBodyTransferSize = NaN;
+        this._responseHeadersTransferSize = NaN;
+        this._responseBodyTransferSize = NaN;
+        this._responseBodySize = NaN;
+        this._cachedResponseBodySize = NaN;
+
+        // Estimated sizes (if backend does not provide metrics).
+        this._estimatedSize = NaN;
+        this._estimatedTransferSize = NaN;
+        this._estimatedResponseHeadersSize = NaN;
+
+        if (this._initiatorSourceCodeLocation && this._initiatorSourceCodeLocation.sourceCode instanceof WI.Resource)
+            this._initiatorSourceCodeLocation.sourceCode.addInitiatedResource(this);
+    }
 
-// Keep these in sync with the "ResourceType" enum defined by the "Page" domain.
-WebInspector.Resource.Type = {
-    Document: "resource-type-document",
-    Stylesheet: "resource-type-stylesheet",
-    Image: "resource-type-image",
-    Font: "resource-type-font",
-    Script: "resource-type-script",
-    XHR: "resource-type-xhr",
-    WebSocket: "resource-type-websocket",
-    Other: "resource-type-other"
-};
+    // Static
 
-// This MIME Type map is private, use WebInspector.Resource.Type.fromMIMEType().
-WebInspector.Resource.Type._mimeTypeMap = {
-    "text/html": WebInspector.Resource.Type.Document,
-    "text/xml": WebInspector.Resource.Type.Document,
-    "text/plain": WebInspector.Resource.Type.Document,
-    "application/xhtml+xml": WebInspector.Resource.Type.Document,
-    "image/svg+xml": WebInspector.Resource.Type.Document,
-
-    "text/css": WebInspector.Resource.Type.Stylesheet,
-    "text/xsl": WebInspector.Resource.Type.Stylesheet,
-    "text/x-less": WebInspector.Resource.Type.Stylesheet,
-    "text/x-sass": WebInspector.Resource.Type.Stylesheet,
-    "text/x-scss": WebInspector.Resource.Type.Stylesheet,
-
-    "application/pdf": WebInspector.Resource.Type.Image,
-
-    "application/x-font-type1": WebInspector.Resource.Type.Font,
-    "application/x-font-ttf": WebInspector.Resource.Type.Font,
-    "application/x-font-woff": WebInspector.Resource.Type.Font,
-    "application/x-truetype-font": WebInspector.Resource.Type.Font,
-
-    "text/javascript": WebInspector.Resource.Type.Script,
-    "text/ecmascript": WebInspector.Resource.Type.Script,
-    "application/javascript": WebInspector.Resource.Type.Script,
-    "application/ecmascript": WebInspector.Resource.Type.Script,
-    "application/x-javascript": WebInspector.Resource.Type.Script,
-    "application/json": WebInspector.Resource.Type.Script,
-    "application/x-json": WebInspector.Resource.Type.Script,
-    "text/x-javascript": WebInspector.Resource.Type.Script,
-    "text/x-json": WebInspector.Resource.Type.Script,
-    "text/javascript1.1": WebInspector.Resource.Type.Script,
-    "text/javascript1.2": WebInspector.Resource.Type.Script,
-    "text/javascript1.3": WebInspector.Resource.Type.Script,
-    "text/jscript": WebInspector.Resource.Type.Script,
-    "text/livescript": WebInspector.Resource.Type.Script,
-    "text/x-livescript": WebInspector.Resource.Type.Script,
-    "text/typescript": WebInspector.Resource.Type.Script,
-    "text/x-clojure": WebInspector.Resource.Type.Script,
-    "text/x-coffeescript": WebInspector.Resource.Type.Script
-};
+    static resolvedType(type, mimeType)
+    {
+        if (type && type !== WI.Resource.Type.Other)
+            return type;
 
-WebInspector.Resource.Type.fromMIMEType = function(mimeType)
-{
-    if (!mimeType)
-        return WebInspector.Resource.Type.Other;
+        return Resource.typeFromMIMEType(mimeType);
+    }
 
-    mimeType = parseMIMEType(mimeType).type;
+    static typeFromMIMEType(mimeType)
+    {
+        if (!mimeType)
+            return WI.Resource.Type.Other;
 
-    if (mimeType in WebInspector.Resource.Type._mimeTypeMap)
-        return WebInspector.Resource.Type._mimeTypeMap[mimeType];
+        mimeType = parseMIMEType(mimeType).type;
 
-    if (mimeType.startsWith("image/"))
-        return WebInspector.Resource.Type.Image;
+        if (mimeType in WI.Resource._mimeTypeMap)
+            return WI.Resource._mimeTypeMap[mimeType];
 
-    if (mimeType.startsWith("font/"))
-        return WebInspector.Resource.Type.Font;
+        if (mimeType.startsWith("image/"))
+            return WI.Resource.Type.Image;
 
-    return WebInspector.Resource.Type.Other;
-};
+        if (mimeType.startsWith("font/"))
+            return WI.Resource.Type.Font;
 
-WebInspector.Resource.Type.displayName = function(type, plural)
-{
-    switch(type) {
-    case WebInspector.Resource.Type.Document:
-        if (plural)
-            return WebInspector.UIString("Documents");
-        return WebInspector.UIString("Document");
-    case WebInspector.Resource.Type.Stylesheet:
-        if (plural)
-            return WebInspector.UIString("Stylesheets");
-        return WebInspector.UIString("Stylesheet");
-    case WebInspector.Resource.Type.Image:
-        if (plural)
-            return WebInspector.UIString("Images");
-        return WebInspector.UIString("Image");
-    case WebInspector.Resource.Type.Font:
-        if (plural)
-            return WebInspector.UIString("Fonts");
-        return WebInspector.UIString("Font");
-    case WebInspector.Resource.Type.Script:
-        if (plural)
-            return WebInspector.UIString("Scripts");
-        return WebInspector.UIString("Script");
-    case WebInspector.Resource.Type.XHR:
-        if (plural)
-            return WebInspector.UIString("XHRs");
-        return WebInspector.UIString("XHR");
-    case WebInspector.Resource.Type.WebSocket:
-        if (plural)
-            return WebInspector.UIString("Sockets");
-        return WebInspector.UIString("Socket");
-    case WebInspector.Resource.Type.Other:
-        return WebInspector.UIString("Other");
-    default:
-        console.error("Unknown resource type: ", type);
-        return null;
+        return WI.Resource.Type.Other;
     }
-};
 
-WebInspector.Resource.prototype = {
-    constructor: WebInspector.Resource,
+    static displayNameForType(type, plural)
+    {
+        switch (type) {
+        case WI.Resource.Type.Document:
+            if (plural)
+                return WI.UIString("Documents");
+            return WI.UIString("Document");
+        case WI.Resource.Type.Stylesheet:
+            if (plural)
+                return WI.UIString("Stylesheets");
+            return WI.UIString("Stylesheet");
+        case WI.Resource.Type.Image:
+            if (plural)
+                return WI.UIString("Images");
+            return WI.UIString("Image");
+        case WI.Resource.Type.Font:
+            if (plural)
+                return WI.UIString("Fonts");
+            return WI.UIString("Font");
+        case WI.Resource.Type.Script:
+            if (plural)
+                return WI.UIString("Scripts");
+            return WI.UIString("Script");
+        case WI.Resource.Type.XHR:
+            if (plural)
+                return WI.UIString("XHRs");
+            return WI.UIString("XHR");
+        case WI.Resource.Type.Fetch:
+            if (plural)
+                return WI.UIString("Fetches", "Resources loaded via 'fetch' method");
+            return WI.repeatedUIString.fetch();
+        case WI.Resource.Type.Ping:
+            if (plural)
+                return WI.UIString("Pings");
+            return WI.UIString("Ping");
+        case WI.Resource.Type.Beacon:
+            if (plural)
+                return WI.UIString("Beacons");
+            return WI.UIString("Beacon");
+        case WI.Resource.Type.WebSocket:
+            if (plural)
+                return WI.UIString("Sockets");
+            return WI.UIString("Socket");
+        case WI.Resource.Type.Other:
+            return WI.UIString("Other");
+        default:
+            console.error("Unknown resource type", type);
+            return null;
+        }
+    }
 
-    // Public
+    static classNameForResource(resource)
+    {
+        if (resource.type === WI.Resource.Type.Other) {
+            if (resource.requestedByteRange)
+                return "resource-type-range";
+        }
+        return resource.type;
+    }
+
+    static displayNameForProtocol(protocol)
+    {
+        switch (protocol) {
+        case "h2":
+            return "HTTP/2";
+        case "http/1.0":
+            return "HTTP/1.0";
+        case "http/1.1":
+            return "HTTP/1.1";
+        case "spdy/2":
+            return "SPDY/2";
+        case "spdy/3":
+            return "SPDY/3";
+        case "spdy/3.1":
+            return "SPDY/3.1";
+        default:
+            return null;
+        }
+    }
+
+    static comparePriority(a, b)
+    {
+        console.assert(typeof a === "symbol");
+        console.assert(typeof b === "symbol");
+
+        const map = {
+            [WI.Resource.NetworkPriority.Unknown]: 0,
+            [WI.Resource.NetworkPriority.Low]: 1,
+            [WI.Resource.NetworkPriority.Medium]: 2,
+            [WI.Resource.NetworkPriority.High]: 3,
+        };
+
+        let aNum = map[a] || 0;
+        let bNum = map[b] || 0;
+        return aNum - bNum;
+    }
+
+    static displayNameForPriority(priority)
+    {
+        switch (priority) {
+        case WI.Resource.NetworkPriority.Low:
+            return WI.UIString("Low");
+        case WI.Resource.NetworkPriority.Medium:
+            return WI.UIString("Medium");
+        case WI.Resource.NetworkPriority.High:
+            return WI.UIString("High");
+        default:
+            return null;
+        }
+    }
+
+    static responseSourceFromPayload(source)
+    {
+        if (!source)
+            return WI.Resource.ResponseSource.Unknown;
+
+        switch (source) {
+        case NetworkAgent.ResponseSource.Unknown:
+            return WI.Resource.ResponseSource.Unknown;
+        case NetworkAgent.ResponseSource.Network:
+            return WI.Resource.ResponseSource.Network;
+        case NetworkAgent.ResponseSource.MemoryCache:
+            return WI.Resource.ResponseSource.MemoryCache;
+        case NetworkAgent.ResponseSource.DiskCache:
+            return WI.Resource.ResponseSource.DiskCache;
+        case NetworkAgent.ResponseSource.ServiceWorker:
+            return WI.Resource.ResponseSource.ServiceWorker;
+        default:
+            console.error("Unknown response source type", source);
+            return WI.Resource.ResponseSource.Unknown;
+        }
+    }
+
+    static networkPriorityFromPayload(priority)
+    {
+        switch (priority) {
+        case NetworkAgent.MetricsPriority.Low:
+            return WI.Resource.NetworkPriority.Low;
+        case NetworkAgent.MetricsPriority.Medium:
+            return WI.Resource.NetworkPriority.Medium;
+        case NetworkAgent.MetricsPriority.High:
+            return WI.Resource.NetworkPriority.High;
+        default:
+            console.error("Unknown metrics priority", priority);
+            return WI.Resource.NetworkPriority.Unknown;
+        }
+    }
 
-    get url()
+    static connectionIdentifierFromPayload(connectionIdentifier)
     {
-        return this._url;
-    },
+        // Map backend connection identifiers to an easier to read number.
+        if (!WI.Resource.connectionIdentifierMap) {
+            WI.Resource.connectionIdentifierMap = new Map;
+            WI.Resource.nextConnectionIdentifier = 1;
+        }
+
+        let id = WI.Resource.connectionIdentifierMap.get(connectionIdentifier);
+        if (id)
+            return id;
+
+        id = WI.Resource.nextConnectionIdentifier++;
+        WI.Resource.connectionIdentifierMap.set(connectionIdentifier, id);
+        return id;
+    }
+
+    // Public
+
+    get url() { return this._url; }
+    get mimeType() { return this._mimeType; }
+    get target() { return this._target; }
+    get type() { return this._type; }
+    get loaderIdentifier() { return this._loaderIdentifier; }
+    get requestIdentifier() { return this._requestIdentifier; }
+    get requestMethod() { return this._requestMethod; }
+    get requestData() { return this._requestData; }
+    get initiatorCallFrames() { return this._initiatorCallFrames; }
+    get initiatorSourceCodeLocation() { return this._initiatorSourceCodeLocation; }
+    get initiatorNode() { return this._initiatorNode; }
+    get initiatedResources() { return this._initiatedResources; }
+    get originalRequestWillBeSentTimestamp() { return this._originalRequestWillBeSentTimestamp; }
+    get statusCode() { return this._statusCode; }
+    get statusText() { return this._statusText; }
+    get responseSource() { return this._responseSource; }
+    get security() { return this._security; }
+    get timingData() { return this._timingData; }
+    get protocol() { return this._protocol; }
+    get priority() { return this._priority; }
+    get remoteAddress() { return this._remoteAddress; }
+    get connectionIdentifier() { return this._connectionIdentifier; }
+    get parentFrame() { return this._parentFrame; }
+    get finished() { return this._finished; }
+    get failed() { return this._failed; }
+    get canceled() { return this._canceled; }
+    get failureReasonText() { return this._failureReasonText; }
+    get requestHeaders() { return this._requestHeaders; }
+    get responseHeaders() { return this._responseHeaders; }
+    get requestSentTimestamp() { return this._requestSentTimestamp; }
+    get requestSentWalltime() { return this._requestSentWalltime; }
+    get responseReceivedTimestamp() { return this._responseReceivedTimestamp; }
+    get lastDataReceivedTimestamp() { return this._lastDataReceivedTimestamp; }
+    get finishedOrFailedTimestamp() { return this._finishedOrFailedTimestamp; }
+    get cached() { return this._cached; }
+    get requestHeadersTransferSize() { return this._requestHeadersTransferSize; }
+    get requestBodyTransferSize() { return this._requestBodyTransferSize; }
+    get responseHeadersTransferSize() { return this._responseHeadersTransferSize; }
+    get responseBodyTransferSize() { return this._responseBodyTransferSize; }
+    get cachedResponseBodySize() { return this._cachedResponseBodySize; }
+    get redirects() { return this._redirects; }
 
     get urlComponents()
     {
         if (!this._urlComponents)
             this._urlComponents = parseURL(this._url);
         return this._urlComponents;
-    },
-
-    get displayName()
-    {
-        return WebInspector.displayNameForURL(this._url, this.urlComponents);
-    },
+    }
 
-    get initiatorSourceCodeLocation()
+    get loadedSecurely()
     {
-        return this._initiatorSourceCodeLocation;
-    },
+        if (this.urlComponents.scheme !== "https" && this.urlComponents.scheme !== "wss" && this.urlComponents.scheme !== "sftp")
+            return false;
+        if (isNaN(this._timingData.secureConnectionStart) && !isNaN(this._timingData.connectionStart))
+            return false;
+        return true;
+    }
 
-    get type()
+    get displayName()
     {
-        return this._type;
-    },
+        return WI.displayNameForURL(this._url, this.urlComponents);
+    }
 
-    get mimeType()
+    get displayURL()
     {
-        return this._mimeType;
-    },
+        const isMultiLine = true;
+        const dataURIMaxSize = 64;
+        return WI.truncateURL(this._url, isMultiLine, dataURIMaxSize);
+    }
 
     get mimeTypeComponents()
     {
         if (!this._mimeTypeComponents)
             this._mimeTypeComponents = parseMIMEType(this._mimeType);
         return this._mimeTypeComponents;
-    },
+    }
 
     get syntheticMIMEType()
     {
         // Resources are often transferred with a MIME-type that doesn't match the purpose the
-        // resource was loaded for, which is what WebInspector.Resource.Type represents.
+        // resource was loaded for, which is what WI.Resource.Type represents.
         // This getter generates a MIME-type, if needed, that matches the resource type.
 
         // If the type matches the Resource.Type of the MIME-type, then return the actual MIME-type.
-        if (this._type === WebInspector.Resource.Type.fromMIMEType(this._mimeType))
+        if (this._type === WI.Resource.typeFromMIMEType(this._mimeType))
             return this._mimeType;
 
         // Return the default MIME-types for the Resource.Type, since the current MIME-type
         // does not match what is expected for the Resource.Type.
         switch (this._type) {
-        case WebInspector.Resource.Type.Document:
-            return "text/html";
-        case WebInspector.Resource.Type.Stylesheet:
+        case WI.Resource.Type.Stylesheet:
             return "text/css";
-        case WebInspector.Resource.Type.Script:
+        case WI.Resource.Type.Script:
             return "text/javascript";
         }
 
         // Return the actual MIME-type since we don't have a better synthesized one to return.
         return this._mimeType;
-    },
+    }
 
-    get contentURL()
+    createObjectURL()
     {
-        const maximumDataURLSize = 1024 * 1024; // 1 MiB
-
-        // If content is not available or won't fit a data URL, fallback to using original URL.
-        var content = this.content;
-        if (content === null || content.length > maximumDataURLSize)
+        // If content is not available, fallback to using original URL.
+        // The client may try to revoke it, but nothing will happen.
+        let content = this.content;
+        if (!content)
             return this._url;
 
-        return "data:" + this.mimeTypeComponents.type + (this.contentIsBase64Encoded ? ";base64," + content : "," + encodeURIComponent(content));
-    },
-
-    isMainResource: function()
-    {
-        return this._parentFrame ? this._parentFrame.mainResource === this : false;
-    },
+        if (content instanceof Blob)
+            return URL.createObjectURL(content);
 
-    get parentFrame()
-    {
-        return this._parentFrame;
-    },
+        if (typeof content === "string") {
+            let blob = textToBlob(content, this._mimeType);
+            return URL.createObjectURL(blob);
+        }
 
-    get loaderIdentifier()
-    {
-        return this._loaderIdentifier;
-    },
+        return null;
+    }
 
-    get requestIdentifier()
+    isMainResource()
     {
-        return this._requestIdentifier;
-    },
+        return this._parentFrame ? this._parentFrame.mainResource === this : false;
+    }
 
-    get finished()
+    addInitiatedResource(resource)
     {
-        return this._finished;
-    },
+        if (!(resource instanceof WI.Resource))
+            return;
 
-    get failed()
-    {
-        return this._failed;
-    },
+        this._initiatedResources.push(resource);
 
-    get canceled()
-    {
-        return this._canceled;
-    },
+        this.dispatchEventToListeners(WI.Resource.Event.InitiatedResourcesDidChange);
+    }
 
-    get requestMethod()
+    get queryStringParameters()
     {
-        return this._requestMethod;
-    },
+        if (this._queryStringParameters === undefined)
+            this._queryStringParameters = parseQueryString(this.urlComponents.queryString, true);
+        return this._queryStringParameters;
+    }
 
-    get requestData()
+    get requestFormParameters()
     {
-        return this._requestData;
-    },
+        if (this._requestFormParameters === undefined)
+            this._requestFormParameters = this.hasRequestFormParameters() ? parseQueryString(this.requestData, true) : null;
+        return this._requestFormParameters;
+    }
 
     get requestDataContentType()
     {
         return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null;
-    },
-
-    get requestHeaders()
-    {
-        return this._requestHeaders;
-    },
+    }
 
-    get responseHeaders()
+    get requestCookies()
     {
-        return this._responseHeaders;
-    },
+        if (!this._requestCookies)
+            this._requestCookies = WI.Cookie.parseCookieRequestHeader(this._requestHeaders.valueForCaseInsensitiveKey("Cookie"));
 
-    get requestSentTimestamp()
-    {
-        return this._requestSentTimestamp;
-    },
+        return this._requestCookies;
+    }
 
-    get lastRedirectReceivedTimestamp()
+    get responseCookies()
     {
-        return this._lastRedirectReceivedTimestamp;
-    },
+        if (!this._responseCookies) {
+            // FIXME: The backend sends multiple "Set-Cookie" headers in one "Set-Cookie" with multiple values
+            // separated by ", ". This doesn't allow us to safely distinguish between a ", " that separates
+            // multiple headers or one that may be valid part of a Cookie's value or attribute, such as the
+            // ", " in the the date format "Expires=Tue, 03-Oct-2017 04:39:21 GMT". To improve heuristics
+            // we do a negative lookahead for numbers, but we can still fail on cookie values containing ", ".
+            let rawCombinedHeader = this._responseHeaders.valueForCaseInsensitiveKey("Set-Cookie") || "";
+            let setCookieHeaders = rawCombinedHeader.split(/, (?![0-9])/);
+            let cookies = [];
+            for (let header of setCookieHeaders) {
+                let cookie = WI.Cookie.parseSetCookieResponseHeader(header);
+                if (cookie)
+                    cookies.push(cookie);
+            }
+            this._responseCookies = cookies;
+        }
 
-    get responseReceivedTimestamp()
-    {
-        return this._responseReceivedTimestamp;
-    },
+        return this._responseCookies;
+    }
 
-    get lastDataReceivedTimestamp()
+    get requestSentDate()
     {
-        return this._lastDataReceivedTimestamp;
-    },
+        return isNaN(this._requestSentWalltime) ? null : new Date(this._requestSentWalltime * 1000);
+    }
 
-    get finishedOrFailedTimestamp()
+    get lastRedirectReceivedTimestamp()
     {
-        return this._finishedOrFailedTimestamp;
-    },
+        return this._redirects.length ? this._redirects.lastValue.timestamp : NaN;
+    }
 
     get firstTimestamp()
     {
-        return this.requestSentTimestamp || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp;
-    },
+        return this.timingData.startTime || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp;
+    }
 
     get lastTimestamp()
     {
-        return this.finishedOrFailedTimestamp || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp;
-    },
-
-    get duration()
-    {
-        return this._finishedOrFailedTimestamp - this._requestSentTimestamp;
-    },
+        return this.timingData.responseEnd || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp;
+    }
 
     get latency()
     {
-        return this._responseReceivedTimestamp - this._requestSentTimestamp;
-    },
+        return this.timingData.responseStart - this.timingData.requestStart;
+    }
 
     get receiveDuration()
     {
-        return this._finishedOrFailedTimestamp - this._responseReceivedTimestamp;
-    },
+        return this.timingData.responseEnd - this.timingData.responseStart;
+    }
 
-    get cached()
+    get totalDuration()
+    {
+        return this.timingData.responseEnd - this.timingData.startTime;
+    }
+
+    get size()
     {
-        return this._cached;
-    },
+        if (!isNaN(this._cachedResponseBodySize))
+            return this._cachedResponseBodySize;
 
-    get statusCode()
+        if (!isNaN(this._responseBodySize) && this._responseBodySize !== 0)
+            return this._responseBodySize;
+
+        return this._estimatedSize;
+    }
+
+    get networkEncodedSize()
     {
-        return this._statusCode;
-    },
+        return this._responseBodyTransferSize;
+    }
 
-    get statusText()
+    get networkDecodedSize()
     {
-        return this._statusText;
-    },
+        return this._responseBodySize;
+    }
 
-    get size()
+    get networkTotalTransferSize()
     {
-        return this._size;
-    },
+        return this._responseHeadersTransferSize + this._responseBodyTransferSize;
+    }
 
-    get encodedSize()
+    get estimatedNetworkEncodedSize()
     {
-        if (!isNaN(this._transferSize))
-            return this._transferSize;
+        let exact = this.networkEncodedSize;
+        if (!isNaN(exact))
+            return exact;
+
+        if (this._cached)
+            return 0;
+
+        // FIXME: <https://webkit.org/b/158463> Network: Correctly report encoded data length (transfer size) from CFNetwork to NetworkResourceLoader
+        // macOS provides the decoded transfer size instead of the encoded size
+        // for estimatedTransferSize. So prefer the "Content-Length" property
+        // on mac if it is available.
+        if (WI.Platform.name === "mac") {
+            let contentLength = Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"));
+            if (!isNaN(contentLength))
+                return contentLength;
+        }
+
+        if (!isNaN(this._estimatedTransferSize))
+            return this._estimatedTransferSize;
 
         // If we did not receive actual transfer size from network
         // stack, we prefer using Content-Length over resourceSize as
@@ -411,34 +571,66 @@ WebInspector.Resource.prototype = {
         // work for chunks with non-trivial encodings. We need a way to
         // get actual transfer size from the network stack.
 
-        return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._size);
-    },
+        return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._estimatedSize);
+    }
 
-    get transferSize()
+    get estimatedTotalTransferSize()
     {
+        let exact = this.networkTotalTransferSize;
+        if (!isNaN(exact))
+            return exact;
+
         if (this.statusCode === 304) // Not modified
-            return this._responseHeadersSize;
+            return this._estimatedResponseHeadersSize;
 
         if (this._cached)
             return 0;
 
-        return this._responseHeadersSize + this.encodedSize;
-    },
+        return this._estimatedResponseHeadersSize + this.estimatedNetworkEncodedSize;
+    }
 
     get compressed()
     {
-        var contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
-        return contentEncoding && /\b(?:gzip|deflate)\b/.test(contentEncoding);
-    },
+        let contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
+        return !!(contentEncoding && /\b(?:gzip|deflate)\b/.test(contentEncoding));
+    }
+
+    get requestedByteRange()
+    {
+        let range = this._requestHeaders.valueForCaseInsensitiveKey("Range");
+        if (!range)
+            return null;
+
+        let rangeValues = range.match(/bytes=(\d+)-(\d+)/);
+        if (!rangeValues)
+            return null;
+
+        let start = parseInt(rangeValues[1]);
+        if (isNaN(start))
+            return null;
+
+        let end = parseInt(rangeValues[2]);
+        if (isNaN(end))
+            return null;
+
+        return {start, end};
+    }
 
     get scripts()
     {
         return this._scripts || [];
-    },
+    }
+
+    get serverTiming()
+    {
+        if (!this._serverTimingEntries)
+            this._serverTimingEntries = WI.ServerTimingEntry.parseHeaders(this._responseHeaders.valueForCaseInsensitiveKey("Server-Timing"));
+        return this._serverTimingEntries;
+    }
 
-    scriptForLocation: function(sourceCodeLocation)
+    scriptForLocation(sourceCodeLocation)
     {
-        console.assert(!(this instanceof WebInspector.SourceMapResource));
+        console.assert(!(this instanceof WI.SourceMapResource));
         console.assert(sourceCodeLocation.sourceCode === this, "SourceCodeLocation must be in this Resource");
         if (sourceCodeLocation.sourceCode !== this)
             return null;
@@ -457,185 +649,264 @@ WebInspector.Resource.prototype = {
         }
 
         return null;
-    },
+    }
 
-    updateForRedirectResponse: function(url, requestHeaders, timestamp)
+    updateForRedirectResponse(request, response, elapsedTime, walltime)
     {
         console.assert(!this._finished);
         console.assert(!this._failed);
         console.assert(!this._canceled);
 
-        var oldURL = this._url;
+        let oldURL = this._url;
+        let oldHeaders = this._requestHeaders;
 
-        this._url = url;
-        this._requestHeaders = requestHeaders || {};
-        this._lastRedirectReceivedTimestamp = timestamp || NaN;
+        if (request.url)
+            this._url = request.url;
 
-        if (oldURL !== url) {
+        this._requestHeaders = request.headers || {};
+        this._requestCookies = null;
+        this._redirects.push(new WI.Redirect(oldURL, request.method, oldHeaders, response.status, response.statusText, response.headers, elapsedTime));
+
+        if (oldURL !== request.url) {
             // Delete the URL components so the URL is re-parsed the next time it is requested.
-            delete this._urlComponents;
+            this._urlComponents = null;
 
-            this.dispatchEventToListeners(WebInspector.Resource.Event.URLDidChange, {oldURL: oldURL});
+            this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
         }
 
-        this.dispatchEventToListeners(WebInspector.Resource.Event.RequestHeadersDidChange);
-        this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
-    },
+        this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
+        this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
+    }
+
+    hasResponse()
+    {
+        return !isNaN(this._statusCode) || this._finished || this._failed;
+    }
 
-    updateForResponse: function(url, mimeType, type, responseHeaders, statusCode, statusText, timestamp)
+    hasRequestFormParameters()
+    {
+        let requestDataContentType = this.requestDataContentType;
+        return requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i);
+    }
+
+    updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, elapsedTime, timingData, source, security)
     {
         console.assert(!this._finished);
         console.assert(!this._failed);
         console.assert(!this._canceled);
 
-        var oldURL = this._url;
-        var oldMIMEType = this._mimeType;
-        var oldType = this._type;
+        let oldURL = this._url;
+        let oldMIMEType = this._mimeType;
+        let oldType = this._type;
 
-        if (type in WebInspector.Resource.Type)
-            type = WebInspector.Resource.Type[type];
+        if (type in WI.Resource.Type)
+            type = WI.Resource.Type[type];
+
+        if (url)
+            this._url = url;
 
-        this._url = url;
         this._mimeType = mimeType;
-        this._type = type || WebInspector.Resource.Type.fromMIMEType(mimeType);
+        this._type = Resource.resolvedType(type, mimeType);
         this._statusCode = statusCode;
         this._statusText = statusText;
         this._responseHeaders = responseHeaders || {};
-        this._responseReceivedTimestamp = timestamp || NaN;
+        this._responseCookies = null;
+        this._serverTimingEntries = null;
+        this._responseReceivedTimestamp = elapsedTime || NaN;
+        this._timingData = WI.ResourceTimingData.fromPayload(timingData, this);
+
+        if (source)
+            this._responseSource = WI.Resource.responseSourceFromPayload(source);
 
-        this._responseHeadersSize = String(this._statusCode).length + this._statusText.length + 12; // Extra length is for "HTTP/1.1 ", " ", and "\r\n".
-        for (var name in this._responseHeaders)
-            this._responseHeadersSize += name.length + this._responseHeaders[name].length + 4; // Extra length is for ": ", and "\r\n".
+        this._security = security || {};
 
-        if (statusCode === 304 && !this._cached)
-            this.markAsCached();
+        const headerBaseSize = 12; // Length of "HTTP/1.1 ", " ", and "\r\n".
+        const headerPad = 4; // Length of ": " and "\r\n".
+        this._estimatedResponseHeadersSize = String(this._statusCode).length + this._statusText.length + headerBaseSize;
+        for (let name in this._responseHeaders)
+            this._estimatedResponseHeadersSize += name.length + this._responseHeaders[name].length + headerPad;
+
+        if (!this._cached) {
+            if (statusCode === 304 || (this._responseSource === WI.Resource.ResponseSource.MemoryCache || this._responseSource === WI.Resource.ResponseSource.DiskCache))
+                this.markAsCached();
+        }
 
         if (oldURL !== url) {
             // Delete the URL components so the URL is re-parsed the next time it is requested.
-            delete this._urlComponents;
+            this._urlComponents = null;
 
-            this.dispatchEventToListeners(WebInspector.Resource.Event.URLDidChange, {oldURL: oldURL});
+            this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
         }
 
         if (oldMIMEType !== mimeType) {
             // Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested.
-            delete this._mimeTypeComponents;
+            this._mimeTypeComponents = null;
 
-            this.dispatchEventToListeners(WebInspector.Resource.Event.MIMETypeDidChange, {oldMIMEType: oldMIMEType});
+            this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType});
         }
 
         if (oldType !== type)
-            this.dispatchEventToListeners(WebInspector.Resource.Event.TypeDidChange, {oldType: oldType});
+            this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
 
-        console.assert(isNaN(this._size));
-        console.assert(isNaN(this._transferSize));
+        console.assert(isNaN(this._estimatedSize));
+        console.assert(isNaN(this._estimatedTransferSize));
 
         // The transferSize becomes 0 when status is 304 or Content-Length is available, so
         // notify listeners of that change.
         if (statusCode === 304 || this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
-            this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
+            this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
+
+        this.dispatchEventToListeners(WI.Resource.Event.ResponseReceived);
+        this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
+    }
+
+    updateWithMetrics(metrics)
+    {
+        this._receivedNetworkLoadMetrics = true;
+
+        if (metrics.protocol)
+            this._protocol = metrics.protocol;
+        if (metrics.priority)
+            this._priority = WI.Resource.networkPriorityFromPayload(metrics.priority);
+        if (metrics.remoteAddress)
+            this._remoteAddress = metrics.remoteAddress;
+        if (metrics.connectionIdentifier)
+            this._connectionIdentifier = WI.Resource.connectionIdentifierFromPayload(metrics.connectionIdentifier);
+        if (metrics.requestHeaders) {
+            this._requestHeaders = metrics.requestHeaders;
+            this._requestCookies = null;
+            this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
+        }
 
-        this.dispatchEventToListeners(WebInspector.Resource.Event.ResponseReceived);
-        this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
-    },
+        if ("requestHeaderBytesSent" in metrics) {
+            this._requestHeadersTransferSize = metrics.requestHeaderBytesSent;
+            this._requestBodyTransferSize = metrics.requestBodyBytesSent;
+            this._responseHeadersTransferSize = metrics.responseHeaderBytesReceived;
+            this._responseBodyTransferSize = metrics.responseBodyBytesReceived;
+            this._responseBodySize = metrics.responseBodyDecodedSize;
+
+            console.assert(this._requestHeadersTransferSize >= 0);
+            console.assert(this._requestBodyTransferSize >= 0);
+            console.assert(this._responseHeadersTransferSize >= 0);
+            console.assert(this._responseBodyTransferSize >= 0);
+            console.assert(this._responseBodySize >= 0);
+
+            // There may have been no size updates received during load if Content-Length was 0.
+            if (isNaN(this._estimatedSize))
+                this._estimatedSize = 0;
+
+            this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize: this._estimatedSize});
+            this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
+        }
+
+        if (metrics.securityConnection) {
+            if (!this._security)
+                this._security = {};
+            this._security.connection = metrics.securityConnection;
+        }
+
+        this.dispatchEventToListeners(WI.Resource.Event.MetricsDidChange);
+    }
 
-    canRequestContentFromBackend: function()
+    setCachedResponseBodySize(size)
     {
-        return this._finished;
-    },
+        console.assert(!isNaN(size), "Size should be a valid number.");
+        console.assert(isNaN(this._cachedResponseBodySize), "This should only be set once.");
+        console.assert(this._estimatedSize === size, "The legacy path was updated already and matches.");
 
-    requestContentFromBackend: function(callback)
+        this._cachedResponseBodySize = size;
+    }
+
+    requestContentFromBackend()
     {
         // If we have the requestIdentifier we can get the actual response for this specific resource.
         // Otherwise the content will be cached resource data, which might not exist anymore.
-        if (this._requestIdentifier) {
-            NetworkAgent.getResponseBody(this._requestIdentifier, callback);
-            return true;
-        }
+        if (this._requestIdentifier)
+            return NetworkAgent.getResponseBody(this._requestIdentifier);
 
-        if (this._parentFrame) {
-            PageAgent.getResourceContent(this._parentFrame.id, this._url, callback);
-            return true;
-        }
+        // There is no request identifier or frame to request content from.
+        if (this._parentFrame)
+            return PageAgent.getResourceContent(this._parentFrame.id, this._url);
 
-        // There is no request identifier or frame to request content from. Return false to cause the
-        // pending callbacks to get null content.
-        return false;
-    },
+        return Promise.reject(new Error("Content request failed."));
+    }
 
-    increaseSize: function(dataLength, timestamp)
+    increaseSize(dataLength, elapsedTime)
     {
         console.assert(dataLength >= 0);
+        console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated size.");
 
-        if (isNaN(this._size))
-            this._size = 0;
+        if (isNaN(this._estimatedSize))
+            this._estimatedSize = 0;
 
-        var previousSize = this._size;
+        let previousSize = this._estimatedSize;
 
-        this._size += dataLength;
+        this._estimatedSize += dataLength;
 
-        this._lastDataReceivedTimestamp = timestamp || NaN;
+        this._lastDataReceivedTimestamp = elapsedTime || NaN;
 
-        this.dispatchEventToListeners(WebInspector.Resource.Event.SizeDidChange, {previousSize: previousSize});
+        this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize});
 
-        // The transferSize is based off of size when status is not 304 or Content-Length is missing.
-        if (isNaN(this._transferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
-            this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
-    },
+        // The estimatedTransferSize is based off of size when status is not 304 or Content-Length is missing.
+        if (isNaN(this._estimatedTransferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
+            this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
+    }
 
-    increaseTransferSize: function(encodedDataLength)
+    increaseTransferSize(encodedDataLength)
     {
         console.assert(encodedDataLength >= 0);
+        console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated transfer size.");
 
-        if (isNaN(this._transferSize))
-            this._transferSize = 0;
-        this._transferSize += encodedDataLength;
+        if (isNaN(this._estimatedTransferSize))
+            this._estimatedTransferSize = 0;
+        this._estimatedTransferSize += encodedDataLength;
 
-        this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
-    },
+        this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
+    }
 
-    markAsCached: function()
+    markAsCached()
     {
         this._cached = true;
 
-        this.dispatchEventToListeners(WebInspector.Resource.Event.CacheStatusDidChange);
+        this.dispatchEventToListeners(WI.Resource.Event.CacheStatusDidChange);
 
-        // The transferSize is starts returning 0 when cached is true, unless status is 304.
+        // The transferSize starts returning 0 when cached is true, unless status is 304.
         if (this._statusCode !== 304)
-            this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
-    },
+            this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
+    }
 
-    markAsFinished: function(timestamp)
+    markAsFinished(elapsedTime)
     {
         console.assert(!this._failed);
         console.assert(!this._canceled);
 
         this._finished = true;
-        this._finishedOrFailedTimestamp = timestamp || NaN;
+        this._finishedOrFailedTimestamp = elapsedTime || NaN;
+        this._timingData.markResponseEndTime(elapsedTime || NaN);
 
-        this.dispatchEventToListeners(WebInspector.Resource.Event.LoadingDidFinish);
-        this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
+        if (this._finishThenRequestContentPromise)
+            this._finishThenRequestContentPromise = null;
 
-        if (this.canRequestContentFromBackend())
-            this.requestContentFromBackendIfNeeded();
-    },
+        this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFinish);
+        this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
+    }
 
-    markAsFailed: function(canceled, timestamp)
+    markAsFailed(canceled, elapsedTime, errorText)
     {
         console.assert(!this._finished);
 
         this._failed = true;
         this._canceled = canceled;
-        this._finishedOrFailedTimestamp = timestamp || NaN;
+        this._finishedOrFailedTimestamp = elapsedTime || NaN;
 
-        this.dispatchEventToListeners(WebInspector.Resource.Event.LoadingDidFail);
-        this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
+        if (!this._failureReasonText)
+            this._failureReasonText = errorText || null;
 
-        // Force the content requests to be serviced. They will get null as the content.
-        this.servicePendingContentRequests(true);
-    },
+        this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFail);
+        this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
+    }
 
-    revertMarkAsFinished: function(timestamp)
+    revertMarkAsFinished()
     {
         console.assert(!this._failed);
         console.assert(!this._canceled);
@@ -643,65 +914,324 @@ WebInspector.Resource.prototype = {
 
         this._finished = false;
         this._finishedOrFailedTimestamp = NaN;
-    },
+    }
+
+    legacyMarkServedFromMemoryCache()
+    {
+        // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the MemoryCache.
+        console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown);
 
-    getImageSize: function(callback)
+        this._responseSource = WI.Resource.ResponseSource.MemoryCache;
+
+        this.markAsCached();
+    }
+
+    legacyMarkServedFromDiskCache()
+    {
+        // COMPATIBILITY (iOS 10.3): This is a legacy code path where we know the resource came from the DiskCache.
+        console.assert(this._responseSource === WI.Resource.ResponseSource.Unknown);
+
+        this._responseSource = WI.Resource.ResponseSource.DiskCache;
+
+        this.markAsCached();
+    }
+
+    isLoading()
+    {
+        return !this._finished && !this._failed;
+    }
+
+    hadLoadingError()
+    {
+        return this._failed || this._canceled || this._statusCode >= 400;
+    }
+
+    getImageSize(callback)
     {
         // Throw an error in the case this resource is not an image.
-        if (this.type !== WebInspector.Resource.Type.Image)
+        if (this.type !== WI.Resource.Type.Image)
             throw "Resource is not an image.";
 
         // See if we've already computed and cached the image size,
         // in which case we can provide them directly.
-        if (this._imageSize) {
+        if (this._imageSize !== undefined) {
             callback(this._imageSize);
             return;
         }
 
+        var objectURL = null;
+
         // Event handler for the image "load" event.
-        function imageDidLoad()
-        {
+        function imageDidLoad() {
+            URL.revokeObjectURL(objectURL);
+
             // Cache the image metrics.
             this._imageSize = {
                 width: image.width,
                 height: image.height
             };
-            
+
             callback(this._imageSize);
-        };
+        }
+
+        function requestContentFailure() {
+            this._imageSize = null;
+            callback(this._imageSize);
+        }
 
         // Create an <img> element that we'll use to load the image resource
         // so that we can query its intrinsic size.
         var image = new Image;
         image.addEventListener("load", imageDidLoad.bind(this), false);
 
-        // Set the image source once we've obtained the base64-encoded URL for it.
-        this.requestContent(function() {
-            image.src = this.contentURL;
-        }.bind(this));
-    },
+        // Set the image source using an object URL once we've obtained its data.
+        this.requestContent().then((content) => {
+            objectURL = image.src = content.sourceCode.createObjectURL();
+            if (!objectURL)
+                requestContentFailure.call(this);
+        }, requestContentFailure.bind(this));
+    }
+
+    requestContent()
+    {
+        if (this._finished)
+            return super.requestContent();
+
+        if (this._failed)
+            return Promise.resolve({error: WI.UIString("An error occurred trying to load the resource.")});
+
+        if (!this._finishThenRequestContentPromise) {
+            this._finishThenRequestContentPromise = new Promise((resolve, reject) => {
+                this.addEventListener(WI.Resource.Event.LoadingDidFinish, resolve);
+                this.addEventListener(WI.Resource.Event.LoadingDidFail, reject);
+            }).then(WI.SourceCode.prototype.requestContent.bind(this));
+        }
+
+        return this._finishThenRequestContentPromise;
+    }
 
-    associateWithScript: function(script)
+    associateWithScript(script)
     {
         if (!this._scripts)
             this._scripts = [];
 
         this._scripts.push(script);
 
-        // COMPATIBILITY (iOS 6): Resources did not know their type until a response
-        // was received. We can set the Resource type to be Script here.
-        if (this._type === WebInspector.Resource.Type.Other) {
-            var oldType = this._type;
-            this._type = WebInspector.Resource.Type.Script;
-            this.dispatchEventToListeners(WebInspector.Resource.Event.TypeDidChange, {oldType: oldType});
+        if (this._type === WI.Resource.Type.Other || this._type === WI.Resource.Type.XHR) {
+            let oldType = this._type;
+            this._type = WI.Resource.Type.Script;
+            this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
+        }
+    }
+
+    saveIdentityToCookie(cookie)
+    {
+        cookie[WI.Resource.URLCookieKey] = this.url.hash;
+        cookie[WI.Resource.MainResourceCookieKey] = this.isMainResource();
+    }
+
+    generateCURLCommand()
+    {
+        function escapeStringPosix(str) {
+            function escapeCharacter(x) {
+                let code = x.charCodeAt(0);
+                let hex = code.toString(16);
+                if (code < 256)
+                    return "\\x" + hex.padStart(2, "0");
+                return "\\u" + hex.padStart(4, "0");
+            }
+
+            if (/[^\x20-\x7E]|'/.test(str)) {
+                // Use ANSI-C quoting syntax.
+                return "$'" + str.replace(/\\/g, "\\\\")
+                                 .replace(/'/g, "\\'")
+                                 .replace(/\n/g, "\\n")
+                                 .replace(/\r/g, "\\r")
+                                 .replace(/!/g, "\\041")
+                                 .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
+            } else {
+                // Use single quote syntax.
+                return `'${str}'`;
+            }
+        }
+
+        let command = ["curl " + escapeStringPosix(this.url).replace(/[[{}\]]/g, "\\$&")];
+        command.push(`-X${this.requestMethod}`);
+
+        for (let key in this.requestHeaders)
+            command.push("-H " + escapeStringPosix(`${key}: ${this.requestHeaders[key]}`));
+
+        if (this.requestDataContentType && this.requestMethod !== "GET" && this.requestData) {
+            if (this.requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
+                command.push("--data " + escapeStringPosix(this.requestData));
+            else
+                command.push("--data-binary " + escapeStringPosix(this.requestData));
+        }
+
+        return command.join(" \\\n");
+    }
+
+    stringifyHTTPRequest()
+    {
+        let lines = [];
+
+        let protocol = this.protocol || "";
+        if (protocol === "h2") {
+            // HTTP/2 Request pseudo headers:
+            // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
+            lines.push(`:method: ${this.requestMethod}`);
+            lines.push(`:scheme: ${this.urlComponents.scheme}`);
+            lines.push(`:authority: ${WI.h2Authority(this.urlComponents)}`);
+            lines.push(`:path: ${WI.h2Path(this.urlComponents)}`);
+        } else {
+            // HTTP/1.1 request line:
+            // https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
+            lines.push(`${this.requestMethod} ${this.urlComponents.path}${protocol ? " " + protocol.toUpperCase() : ""}`);
+        }
+
+        for (let key in this.requestHeaders)
+            lines.push(`${key}: ${this.requestHeaders[key]}`);
+
+        return lines.join("\n") + "\n";
+    }
+
+    stringifyHTTPResponse()
+    {
+        let lines = [];
+
+        let protocol = this.protocol || "";
+        if (protocol === "h2") {
+            // HTTP/2 Response pseudo headers:
+            // https://tools.ietf.org/html/rfc7540#section-8.1.2.4
+            lines.push(`:status: ${this.statusCode}`);
+        } else {
+            // HTTP/1.1 response status line:
+            // https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
+            lines.push(`${protocol ? protocol.toUpperCase() + " " : ""}${this.statusCode} ${this.statusText}`);
         }
-    },
 
-    saveIdentityToCookie: function(cookie)
+        for (let key in this.responseHeaders)
+            lines.push(`${key}: ${this.responseHeaders[key]}`);
+
+        return lines.join("\n") + "\n";
+    }
+
+    async showCertificate()
     {
-        cookie[WebInspector.Resource.URLCookieKey] = this.url.hash;
-        cookie[WebInspector.Resource.MainResourceCookieKey] = this.isMainResource();
+        let errorString = WI.UIString("Unable to show certificate for \u201C%s\u201D").format(this.url);
+
+        try {
+            let {serializedCertificate} = await NetworkAgent.getSerializedCertificate(this._requestIdentifier);
+            if (InspectorFrontendHost.showCertificate(serializedCertificate))
+                return;
+        } catch (e) {
+            console.error(e);
+            throw errorString;
+        }
+
+        let consoleMessage = new WI.ConsoleMessage(this._target, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, errorString);
+        consoleMessage.shouldRevealConsole = true;
+        WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
+
+        throw errorString;
     }
 };
 
-WebInspector.Resource.prototype.__proto__ = WebInspector.SourceCode.prototype;
+WI.Resource.TypeIdentifier = "resource";
+WI.Resource.URLCookieKey = "resource-url";
+WI.Resource.MainResourceCookieKey = "resource-is-main-resource";
+
+WI.Resource.Event = {
+    URLDidChange: "resource-url-did-change",
+    MIMETypeDidChange: "resource-mime-type-did-change",
+    TypeDidChange: "resource-type-did-change",
+    RequestHeadersDidChange: "resource-request-headers-did-change",
+    ResponseReceived: "resource-response-received",
+    LoadingDidFinish: "resource-loading-did-finish",
+    LoadingDidFail: "resource-loading-did-fail",
+    TimestampsDidChange: "resource-timestamps-did-change",
+    SizeDidChange: "resource-size-did-change",
+    TransferSizeDidChange: "resource-transfer-size-did-change",
+    CacheStatusDidChange: "resource-cached-did-change",
+    MetricsDidChange: "resource-metrics-did-change",
+    InitiatedResourcesDidChange: "resource-initiated-resources-did-change",
+};
+
+// Keep these in sync with the "ResourceType" enum defined by the "Page" domain.
+WI.Resource.Type = {
+    Document: "resource-type-document",
+    Stylesheet: "resource-type-stylesheet",
+    Image: "resource-type-image",
+    Font: "resource-type-font",
+    Script: "resource-type-script",
+    XHR: "resource-type-xhr",
+    Fetch: "resource-type-fetch",
+    Ping: "resource-type-ping",
+    Beacon: "resource-type-beacon",
+    WebSocket: "resource-type-websocket",
+    Other: "resource-type-other",
+};
+
+WI.Resource.ResponseSource = {
+    Unknown: Symbol("unknown"),
+    Network: Symbol("network"),
+    MemoryCache: Symbol("memory-cache"),
+    DiskCache: Symbol("disk-cache"),
+    ServiceWorker: Symbol("service-worker"),
+};
+
+WI.Resource.NetworkPriority = {
+    Unknown: Symbol("unknown"),
+    Low: Symbol("low"),
+    Medium: Symbol("medium"),
+    High: Symbol("high"),
+};
+
+WI.Resource.GroupingMode = {
+    Path: "group-resource-by-path",
+    Type: "group-resource-by-type",
+};
+WI.settings.resourceGroupingMode = new WI.Setting("resource-grouping-mode", WI.Resource.GroupingMode.Type);
+
+// This MIME Type map is private, use WI.Resource.typeFromMIMEType().
+WI.Resource._mimeTypeMap = {
+    "text/html": WI.Resource.Type.Document,
+    "text/xml": WI.Resource.Type.Document,
+    "text/plain": WI.Resource.Type.Document,
+    "application/xhtml+xml": WI.Resource.Type.Document,
+
+    "text/css": WI.Resource.Type.Stylesheet,
+    "text/xsl": WI.Resource.Type.Stylesheet,
+    "text/x-less": WI.Resource.Type.Stylesheet,
+    "text/x-sass": WI.Resource.Type.Stylesheet,
+    "text/x-scss": WI.Resource.Type.Stylesheet,
+
+    "application/pdf": WI.Resource.Type.Image,
+    "image/svg+xml": WI.Resource.Type.Image,
+
+    "application/x-font-type1": WI.Resource.Type.Font,
+    "application/x-font-ttf": WI.Resource.Type.Font,
+    "application/x-font-woff": WI.Resource.Type.Font,
+    "application/x-truetype-font": WI.Resource.Type.Font,
+
+    "text/javascript": WI.Resource.Type.Script,
+    "text/ecmascript": WI.Resource.Type.Script,
+    "application/javascript": WI.Resource.Type.Script,
+    "application/ecmascript": WI.Resource.Type.Script,
+    "application/x-javascript": WI.Resource.Type.Script,
+    "application/json": WI.Resource.Type.Script,
+    "application/x-json": WI.Resource.Type.Script,
+    "text/x-javascript": WI.Resource.Type.Script,
+    "text/x-json": WI.Resource.Type.Script,
+    "text/javascript1.1": WI.Resource.Type.Script,
+    "text/javascript1.2": WI.Resource.Type.Script,
+    "text/javascript1.3": WI.Resource.Type.Script,
+    "text/jscript": WI.Resource.Type.Script,
+    "text/livescript": WI.Resource.Type.Script,
+    "text/x-livescript": WI.Resource.Type.Script,
+    "text/typescript": WI.Resource.Type.Script,
+    "text/typescript-jsx": WI.Resource.Type.Script,
+    "text/jsx": WI.Resource.Type.Script,
+    "text/x-clojure": WI.Resource.Type.Script,
+    "text/x-coffeescript": WI.Resource.Type.Script,
+};