197e90a17a6847caf50fa48866cd24239b34bc56
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceHeadersContentView.js
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.ResourceHeadersContentView = class ResourceHeadersContentView extends WI.ContentView
27 {
28     constructor(resource, delegate)
29     {
30         super(null);
31
32         console.assert(resource instanceof WI.Resource);
33
34         this._resource = resource;
35         this._resource.addEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this);
36         this._resource.addEventListener(WI.Resource.Event.RequestHeadersDidChange, this._resourceRequestHeadersDidChange, this);
37         this._resource.addEventListener(WI.Resource.Event.ResponseReceived, this._resourceResponseReceived, this);
38
39         this._delegate = delegate || null;
40
41         this.element.classList.add("resource-details", "resource-headers");
42
43         this._needsSummaryRefresh = false;
44         this._needsRequestHeadersRefresh = false;
45         this._needsResponseHeadersRefresh = false;
46     }
47
48     // Protected
49
50     initialLayout()
51     {
52         super.initialLayout();
53
54         this._summarySection = new WI.ResourceDetailsSection(WI.UIString("Summary"), "summary");
55         this.element.appendChild(this._summarySection.element);
56         this._refreshSummarySection();
57
58         this._requestHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "headers");
59         this.element.appendChild(this._requestHeadersSection.element);
60         this._refreshRequestHeadersSection();
61
62         // FIXME: <https://webkit.org/b/150005> Web Inspector: Redirect requests are not shown in either Network or Timeline tabs
63
64         this._responseHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Response"), "headers");
65         this.element.appendChild(this._responseHeadersSection.element);
66         this._refreshResponseHeadersSection();
67
68         if (this._resource.urlComponents.queryString) {
69             this._queryStringSection = new WI.ResourceDetailsSection(WI.UIString("Query String"));
70             this.element.appendChild(this._queryStringSection.element);
71             this._refreshQueryStringSection();
72         }
73
74         if (this._resource.requestData) {
75             this._requestDataSection = new WI.ResourceDetailsSection(WI.UIString("Request Data"));
76             this.element.appendChild(this._requestDataSection.element);
77             this._refreshRequestDataSection();
78         }
79
80         this._needsSummaryRefresh = false;
81         this._needsRequestHeadersRefresh = false;
82         this._needsResponseHeadersRefresh = false;
83     }
84
85     layout()
86     {
87         super.layout();
88
89         if (this._needsSummaryRefresh) {
90             this._refreshSummarySection();
91             this._needsSummaryRefresh = false;
92         }
93
94         if (this._needsRequestHeadersRefresh) {
95             this._refreshRequestHeadersSection();
96             this._needsRequestHeadersRefresh = false;
97         }
98
99         if (this._needsResponseHeadersRefresh) {
100             this._refreshResponseHeadersSection();
101             this._needsResponseHeadersRefresh = false;
102         }
103     }
104
105     closed()
106     {
107         this._resource.removeEventListener(null, null, this);
108
109         super.closed();
110     }
111
112     // Private
113
114     _incompleteSectionWithMessage(section, message)
115     {
116         section.toggleIncomplete(true);
117
118         let p = section.detailsElement.appendChild(document.createElement("p"));
119         p.textContent = message;
120     }
121
122     _incompleteSectionWithLoadingIndicator(section)
123     {
124         section.toggleIncomplete(true);
125
126         let p = section.detailsElement.appendChild(document.createElement("p"));
127         let spinner = new WI.IndeterminateProgressSpinner;
128         p.appendChild(spinner.element);
129     }
130
131     _appendKeyValuePair(parentElement, key, value, className)
132     {
133         let p = parentElement.appendChild(document.createElement("p"));
134         p.className = "pair";
135         if (className)
136             p.classList.add(className);
137
138         // Don't include a colon if no value.
139         console.assert(typeof key === "string");
140         let displayKey = key + (!!value ? ": " : "");
141
142         let keyElement = p.appendChild(document.createElement("span"));
143         keyElement.className = "key";
144         keyElement.textContent = displayKey;
145
146         let valueElement = p.appendChild(document.createElement("span"));
147         valueElement.className = "value";
148         if (value instanceof Node)
149             valueElement.appendChild(value);
150         else
151             valueElement.textContent = value;
152     }
153
154     _responseSourceDisplayString(responseSource)
155     {
156         switch (responseSource) {
157         case WI.Resource.ResponseSource.Network:
158             return WI.UIString("Network");
159         case WI.Resource.ResponseSource.MemoryCache:
160             return WI.UIString("Memory Cache");
161         case WI.Resource.ResponseSource.DiskCache:
162             return WI.UIString("Disk Cache");
163         case WI.Resource.ResponseSource.Unknown:
164         default:
165             return null;
166         }
167     }
168
169     _refreshSummarySection()
170     {
171         let detailsElement = this._summarySection.detailsElement;
172         detailsElement.removeChildren();
173
174         this._summarySection.toggleError(this._resource.hadLoadingError());
175
176         this._appendKeyValuePair(detailsElement, WI.UIString("URL"), this._resource.url.insertWordBreakCharacters());
177
178         let status = emDash;
179         if (this._resource.hasResponse())
180             status = this._resource.statusCode + (this._resource.statusText ? " " + this._resource.statusText : "");
181         this._appendKeyValuePair(detailsElement, WI.UIString("Status"), status);
182
183         let source = this._responseSourceDisplayString(this._resource.responseSource) || emDash;
184         this._appendKeyValuePair(detailsElement, WI.UIString("Source"), source);
185     }
186
187     _refreshRequestHeadersSection()
188     {
189         let detailsElement = this._requestHeadersSection.detailsElement;
190         detailsElement.removeChildren();
191
192         // A revalidation request still sends a request even though we served from cache, so show the request.
193         if (this._resource.statusCode !== 304) {
194             if (this._resource.responseSource === WI.Resource.ResponseSource.MemoryCache) {
195                 this._incompleteSectionWithMessage(this._requestHeadersSection, WI.UIString("No request, served from the memory cache."));
196                 return;
197             }
198             if (this._resource.responseSource === WI.Resource.ResponseSource.DiskCache) {
199                 this._incompleteSectionWithMessage(this._requestHeadersSection, WI.UIString("No request, served from the disk cache."));
200                 return;
201             }
202         }
203
204         let protocol = this._resource.protocol || "";
205         let urlComponents = this._resource.urlComponents;
206         if (protocol.startsWith("http/1")) {
207             // HTTP/1.1 request line:
208             // https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
209             let requestLine = `${this._resource.requestMethod} ${urlComponents.path} ${protocol.toUpperCase()}`
210             this._appendKeyValuePair(detailsElement, requestLine, null, "h1-status");
211         } else if (protocol === "h2") {
212             // HTTP/2 Request pseudo headers:
213             // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
214             this._appendKeyValuePair(detailsElement, ":method", this._resource.requestMethod, "h2-pseudo-header");
215             this._appendKeyValuePair(detailsElement, ":scheme", urlComponents.scheme, "h2-pseudo-header");
216             this._appendKeyValuePair(detailsElement, ":authority", WI.h2Authority(urlComponents), "h2-pseudo-header");
217             this._appendKeyValuePair(detailsElement, ":path", WI.h2Path(urlComponents), "h2-pseudo-header");
218         }
219
220         let requestHeaders = this._resource.requestHeaders;
221         for (let key in requestHeaders)
222             this._appendKeyValuePair(detailsElement, key, requestHeaders[key], "header");
223
224         if (!detailsElement.firstChild)
225             this._incompleteSectionWithMessage(this._requestHeadersSection, WI.UIString("No request headers"));
226     }
227
228     _refreshResponseHeadersSection()
229     {
230         let detailsElement = this._responseHeadersSection.detailsElement;
231         detailsElement.removeChildren();
232
233         if (!this._resource.hasResponse()) {
234             this._incompleteSectionWithLoadingIndicator(this._responseHeadersSection);
235             return;
236         }
237
238         this._responseHeadersSection.toggleIncomplete(false);
239
240         let protocol = this._resource.protocol || "";
241         if (protocol.startsWith("http/1")) {
242             // HTTP/1.1 response status line:
243             // https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
244             let responseLine = `${protocol.toUpperCase()} ${this._resource.statusCode} ${this._resource.statusText}`;
245             this._appendKeyValuePair(detailsElement, responseLine, null, "h1-status");
246         } else if (protocol === "h2") {
247             // HTTP/2 Response pseudo headers:
248             // https://tools.ietf.org/html/rfc7540#section-8.1.2.4
249             this._appendKeyValuePair(detailsElement, ":status", this._resource.statusCode, "h2-pseudo-header");
250         }
251
252         let responseHeaders = this._resource.responseHeaders;
253         for (let key in responseHeaders)
254             this._appendKeyValuePair(detailsElement, key, responseHeaders[key], "header");
255
256         if (!detailsElement.firstChild)
257             this._incompleteSectionWithMessage(this._responseHeadersSection, WI.UIString("No response headers"));
258     }
259
260     _refreshQueryStringSection()
261     {
262         if (!this._queryStringSection)
263             return;
264
265         let detailsElement = this._queryStringSection.detailsElement;
266         detailsElement.removeChildren();
267
268         let queryString = this._resource.urlComponents.queryString;
269         let queryStringPairs = parseQueryString(queryString, true);
270         for (let {name, value} of queryStringPairs)
271             this._appendKeyValuePair(detailsElement, name, value);
272     }
273
274     _refreshRequestDataSection()
275     {
276         if (!this._requestDataSection)
277             return;
278
279         let detailsElement = this._requestDataSection.detailsElement;
280         detailsElement.removeChildren();
281
282         let requestData = this._resource.requestData;
283         let requestDataContentType = this._resource.requestDataContentType || "";
284
285         if (requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) {
286             // Simple form data that should be parsable like a query string.
287             this._appendKeyValuePair(detailsElement, WI.UIString("MIME Type"), requestDataContentType);
288             let queryStringPairs = parseQueryString(requestData, true)
289             for (let {name, value} of queryStringPairs)
290                 this._appendKeyValuePair(detailsElement, name, value);
291             return;
292         }
293
294         let mimeTypeComponents = parseMIMEType(requestDataContentType);
295         let mimeType = mimeTypeComponents.type;
296         let boundary = mimeTypeComponents.boundary;
297         let encoding = mimeTypeComponents.encoding;
298
299         this._appendKeyValuePair(detailsElement, WI.UIString("MIME Type"), mimeType);
300         if (boundary)
301             this._appendKeyValuePair(detailsElement, WI.UIString("Boundary"), boundary);
302         if (encoding)
303             this._appendKeyValuePair(detailsElement, WI.UIString("Encoding"), encoding);
304
305         let goToButton = detailsElement.appendChild(WI.createGoToArrowButton());
306         goToButton.addEventListener("click", this._goToRequestDataClicked.bind(this));
307         this._appendKeyValuePair(detailsElement, WI.UIString("Request Data"), goToButton);
308     }
309
310     _resourceMetricsDidChange(event)
311     {
312         this._needsRequestHeadersRefresh = true;
313         this._needsResponseHeadersRefresh = true;
314         this.needsLayout();
315     }
316
317     _resourceRequestHeadersDidChange(event)
318     {
319         this._needsRequestHeadersRefresh = true;
320         this.needsLayout();
321     }
322
323     _resourceResponseReceived(event)
324     {
325         this._needsSummaryRefresh = true;
326         this._needsResponseHeadersRefresh = true;
327         this.needsLayout();
328     }
329
330     _goToRequestDataClicked(event)
331     {
332         if (this._delegate)
333             this._delegate.headersContentViewGoToRequestData(this);
334     }
335 };