Web Inspector: Initiated section of Resource Details Sidebar should not display as...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceDetailsSidebarPanel.js
1 /*
2  * Copyright (C) 2013, 2015 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 WebInspector.ResourceDetailsSidebarPanel = class ResourceDetailsSidebarPanel extends WebInspector.DetailsSidebarPanel
27 {
28     constructor()
29     {
30         super("resource-details", WebInspector.UIString("Resource"), WebInspector.UIString("Resource"));
31
32         this.element.classList.add("resource");
33
34         this._resource = null;
35
36         this._typeMIMETypeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("MIME Type"));
37         this._typeResourceTypeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Resource Type"));
38
39         this._typeSection = new WebInspector.DetailsSection("resource-type", WebInspector.UIString("Type"));
40         this._typeSection.groups = [new WebInspector.DetailsSectionGroup([this._typeMIMETypeRow, this._typeResourceTypeRow])];
41
42         this._locationFullURLRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Full URL"));
43         this._locationSchemeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Scheme"));
44         this._locationHostRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Host"));
45         this._locationPortRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Port"));
46         this._locationPathRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Path"));
47         this._locationQueryStringRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Query String"));
48         this._locationFragmentRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Fragment"));
49         this._locationFilenameRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Filename"));
50         this._initiatorRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Initiator"));
51         this._initiatedRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Initiated"));
52
53         var firstGroup = [this._locationFullURLRow];
54         var secondGroup = [this._locationSchemeRow, this._locationHostRow, this._locationPortRow, this._locationPathRow,
55             this._locationQueryStringRow, this._locationFragmentRow, this._locationFilenameRow];
56         var thirdGroup = [this._initiatorRow, this._initiatedRow];
57
58         this._fullURLGroup = new WebInspector.DetailsSectionGroup(firstGroup);
59         this._locationURLComponentsGroup = new WebInspector.DetailsSectionGroup(secondGroup);
60         this._relatedResourcesGroup = new WebInspector.DetailsSectionGroup(thirdGroup);
61
62         this._locationSection = new WebInspector.DetailsSection("resource-location", WebInspector.UIString("Location"), [this._fullURLGroup, this._locationURLComponentsGroup, this._relatedResourcesGroup]);
63
64         this._queryParametersRow = new WebInspector.DetailsSectionDataGridRow(null, WebInspector.UIString("No Query Parameters"));
65         this._queryParametersSection = new WebInspector.DetailsSection("resource-query-parameters", WebInspector.UIString("Query Parameters"));
66         this._queryParametersSection.groups = [new WebInspector.DetailsSectionGroup([this._queryParametersRow])];
67
68         this._requestDataSection = new WebInspector.DetailsSection("resource-request-data", WebInspector.UIString("Request Data"));
69
70         this._requestMethodRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Method"));
71         this._cachedRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Cached"));
72
73         this._statusTextRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Status"));
74         this._statusCodeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Code"));
75
76         this._encodedSizeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Encoded"));
77         this._decodedSizeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Decoded"));
78         this._transferSizeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Transfered"));
79
80         this._compressedRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Compressed"));
81         this._compressionRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Compression"));
82
83         var requestGroup = new WebInspector.DetailsSectionGroup([this._requestMethodRow, this._cachedRow]);
84         var statusGroup = new WebInspector.DetailsSectionGroup([this._statusTextRow, this._statusCodeRow]);
85         var sizeGroup = new WebInspector.DetailsSectionGroup([this._encodedSizeRow, this._decodedSizeRow, this._transferSizeRow]);
86         var compressionGroup = new WebInspector.DetailsSectionGroup([this._compressedRow, this._compressionRow]);
87
88         this._requestAndResponseSection = new WebInspector.DetailsSection("resource-request-response", WebInspector.UIString("Request & Response"), [requestGroup, statusGroup, sizeGroup, compressionGroup]);
89
90         this._requestHeadersRow = new WebInspector.DetailsSectionDataGridRow(null, WebInspector.UIString("No Request Headers"));
91         this._requestHeadersSection = new WebInspector.DetailsSection("resource-request-headers", WebInspector.UIString("Request Headers"));
92         this._requestHeadersSection.groups = [new WebInspector.DetailsSectionGroup([this._requestHeadersRow])];
93
94         this._responseHeadersRow = new WebInspector.DetailsSectionDataGridRow(null, WebInspector.UIString("No Response Headers"));
95         this._responseHeadersSection = new WebInspector.DetailsSection("resource-response-headers", WebInspector.UIString("Response Headers"));
96         this._responseHeadersSection.groups = [new WebInspector.DetailsSectionGroup([this._responseHeadersRow])];
97
98         // Rows for the "Image Size" section.
99         this._imageWidthRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Width"));
100         this._imageHeightRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Height"));
101
102         // "Image Size" section where we display intrinsic metrics for image resources.
103         this._imageSizeSection = new WebInspector.DetailsSection("resource-type", WebInspector.UIString("Image Size"));
104         this._imageSizeSection.groups = [new WebInspector.DetailsSectionGroup([this._imageWidthRow, this._imageHeightRow])];
105
106         this.contentView.element.appendChild(this._typeSection.element);
107         this.contentView.element.appendChild(this._locationSection.element);
108         this.contentView.element.appendChild(this._requestAndResponseSection.element);
109         this.contentView.element.appendChild(this._requestHeadersSection.element);
110         this.contentView.element.appendChild(this._responseHeadersSection.element);
111     }
112
113     // Public
114
115     inspect(objects)
116     {
117         // Convert to a single item array if needed.
118         if (!(objects instanceof Array))
119             objects = [objects];
120
121         var resourceToInspect = null;
122
123         // Iterate over the objects to find a WebInspector.Resource to inspect.
124         for (var i = 0; i < objects.length; ++i) {
125             if (objects[i] instanceof WebInspector.Resource) {
126                 resourceToInspect = objects[i];
127                 break;
128             }
129
130             if (objects[i] instanceof WebInspector.Frame) {
131                 resourceToInspect = objects[i].mainResource;
132                 break;
133             }
134         }
135
136         this.resource = resourceToInspect;
137
138         return !!this._resource;
139     }
140
141     get resource()
142     {
143         return this._resource;
144     }
145
146     set resource(resource)
147     {
148         if (resource === this._resource)
149             return;
150
151         if (this._resource) {
152             this._resource.removeEventListener(WebInspector.Resource.Event.URLDidChange, this._refreshURL, this);
153             this._resource.removeEventListener(WebInspector.Resource.Event.MIMETypeDidChange, this._refreshMIMEType, this);
154             this._resource.removeEventListener(WebInspector.Resource.Event.TypeDidChange, this._refreshResourceType, this);
155             this._resource.removeEventListener(WebInspector.Resource.Event.RequestHeadersDidChange, this._refreshRequestHeaders, this);
156             this._resource.removeEventListener(WebInspector.Resource.Event.ResponseReceived, this._refreshRequestAndResponse, this);
157             this._resource.removeEventListener(WebInspector.Resource.Event.CacheStatusDidChange, this._refreshRequestAndResponse, this);
158             this._resource.removeEventListener(WebInspector.Resource.Event.SizeDidChange, this._refreshDecodedSize, this);
159             this._resource.removeEventListener(WebInspector.Resource.Event.TransferSizeDidChange, this._refreshTransferSize, this);
160             this._resource.removeEventListener(WebInspector.Resource.Event.InitiatedResourcesDidChange, this._refreshRelatedResourcesSection, this);
161         }
162
163         this._resource = resource;
164
165         if (this._resource) {
166             this._resource.addEventListener(WebInspector.Resource.Event.URLDidChange, this._refreshURL, this);
167             this._resource.addEventListener(WebInspector.Resource.Event.MIMETypeDidChange, this._refreshMIMEType, this);
168             this._resource.addEventListener(WebInspector.Resource.Event.TypeDidChange, this._refreshResourceType, this);
169             this._resource.addEventListener(WebInspector.Resource.Event.RequestHeadersDidChange, this._refreshRequestHeaders, this);
170             this._resource.addEventListener(WebInspector.Resource.Event.ResponseReceived, this._refreshRequestAndResponse, this);
171             this._resource.addEventListener(WebInspector.Resource.Event.CacheStatusDidChange, this._refreshRequestAndResponse, this);
172             this._resource.addEventListener(WebInspector.Resource.Event.SizeDidChange, this._refreshDecodedSize, this);
173             this._resource.addEventListener(WebInspector.Resource.Event.TransferSizeDidChange, this._refreshTransferSize, this);
174             this._resource.addEventListener(WebInspector.Resource.Event.InitiatedResourcesDidChange, this._refreshRelatedResourcesSection, this);
175         }
176
177         this.needsRefresh();
178     }
179
180     refresh()
181     {
182         if (!this._resource)
183             return;
184
185         this._refreshURL();
186         this._refreshMIMEType();
187         this._refreshResourceType();
188         this._refreshRequestAndResponse();
189         this._refreshDecodedSize();
190         this._refreshTransferSize();
191         this._refreshRequestHeaders();
192         this._refreshImageSizeSection();
193         this._refreshRequestDataSection();
194         this._refreshRelatedResourcesSection();
195     }
196
197     // Private
198
199     _refreshURL()
200     {
201         if (!this._resource)
202             return;
203
204         this._locationFullURLRow.value = this._resource.url.insertWordBreakCharacters();
205
206         var urlComponents = this._resource.urlComponents;
207         if (urlComponents.scheme) {
208             this._locationSection.groups = [this._fullURLGroup, this._locationURLComponentsGroup, this._relatedResourcesGroup];
209
210             this._locationSchemeRow.value = urlComponents.scheme ? urlComponents.scheme : null;
211             this._locationHostRow.value = urlComponents.host ? urlComponents.host : null;
212             this._locationPortRow.value = urlComponents.port ? urlComponents.port : null;
213             this._locationPathRow.value = urlComponents.path ? urlComponents.path.insertWordBreakCharacters() : null;
214             this._locationQueryStringRow.value = urlComponents.queryString ? urlComponents.queryString.insertWordBreakCharacters() : null;
215             this._locationFragmentRow.value = urlComponents.fragment ? urlComponents.fragment.insertWordBreakCharacters() : null;
216             this._locationFilenameRow.value = urlComponents.lastPathComponent ? urlComponents.lastPathComponent.insertWordBreakCharacters() : null;
217         } else {
218             this._locationSection.groups = [this._fullURLGroup, this._relatedResourcesGroup];
219         }
220
221         if (urlComponents.queryString) {
222             // Ensure the "Query Parameters" section is displayed, right after the "Request & Response" section.
223             this.contentView.element.insertBefore(this._queryParametersSection.element, this._requestAndResponseSection.element.nextSibling);
224
225             this._queryParametersRow.dataGrid = this._createNameValueDataGrid(parseQueryString(urlComponents.queryString, true));
226         } else {
227             // Hide the "Query Parameters" section if we don't have a query string.
228             var queryParametersSectionElement = this._queryParametersSection.element;
229             if (queryParametersSectionElement.parentNode)
230                 queryParametersSectionElement.parentNode.removeChild(queryParametersSectionElement);
231         }
232     }
233
234     _refreshRelatedResourcesSection()
235     {
236         // Hide the section if we don't have anything to show.
237         let groups = this._locationSection.groups;
238         let isSectionVisible = groups.includes(this._relatedResourcesGroup);
239         if (!this._resource.initiatorSourceCodeLocation && !this._resource.initiatedResources.length) {
240             if (isSectionVisible) {
241                 groups.remove(this._relatedResourcesGroup);
242                 this._locationSection.groups = groups;
243             }
244             return;
245         }
246
247         if (!isSectionVisible) {
248             groups.push(this._relatedResourcesGroup);
249             this._locationSection.groups = groups;
250         }
251
252         let initiatorLocation = this._resource.initiatorSourceCodeLocation;
253         this._initiatorRow.value = initiatorLocation ? WebInspector.createSourceCodeLocationLink(initiatorLocation, true) : null;
254
255         let initiatedResources = this._resource.initiatedResources;
256         if (initiatedResources.length) {
257             let resourceLinkContainer = document.createElement("div");
258             for (let resource of initiatedResources)
259                 resourceLinkContainer.appendChild(WebInspector.createResourceLink(resource));
260
261             this._initiatedRow.value = resourceLinkContainer;
262         } else
263             this._initiatedRow.value = null;
264     }
265
266     _refreshResourceType()
267     {
268         if (!this._resource)
269             return;
270
271         this._typeResourceTypeRow.value = WebInspector.Resource.displayNameForType(this._resource.type);
272     }
273
274     _refreshMIMEType()
275     {
276         if (!this._resource)
277             return;
278
279         this._typeMIMETypeRow.value = this._resource.mimeType;
280     }
281
282     _refreshRequestAndResponse()
283     {
284         var resource = this._resource;
285         if (!resource)
286             return;
287
288         // If we don't have a value, we set an em-dash to keep the row from hiding.
289         // This keeps the UI from shifting around as data comes in.
290         var emDash = "\u2014";
291
292         this._requestMethodRow.value = resource.requestMethod || emDash;
293
294         this._cachedRow.value = resource.cached ? WebInspector.UIString("Yes") : WebInspector.UIString("No");
295
296         this._statusCodeRow.value = resource.statusCode || emDash;
297         this._statusTextRow.value = resource.statusText || emDash;
298
299         this._refreshResponseHeaders();
300         this._refreshCompressed();
301     }
302
303     _valueForSize(size)
304     {
305         // If we don't have a value, we set an em-dash to keep the row from hiding.
306         // This keeps the UI from shifting around as data comes in.
307         var emDash = "\u2014";
308         return size > 0 ? Number.bytesToString(size) : emDash;
309     }
310
311     _refreshCompressed()
312     {
313         this._compressedRow.value = this._resource.compressed ? WebInspector.UIString("Yes") : WebInspector.UIString("No");
314         this._compressionRow.value = this._resource.compressed ? WebInspector.UIString("%.2f\u00d7").format(this._resource.size / this._resource.encodedSize) : null;
315     }
316
317     _refreshDecodedSize()
318     {
319         if (!this._resource)
320             return;
321
322         this._encodedSizeRow.value = this._valueForSize(this._resource.encodedSize);
323         this._decodedSizeRow.value = this._valueForSize(this._resource.size);
324
325         this._refreshCompressed();
326     }
327
328     _refreshTransferSize()
329     {
330         if (!this._resource)
331             return;
332
333         this._encodedSizeRow.value = this._valueForSize(this._resource.encodedSize);
334         this._transferSizeRow.value = this._valueForSize(this._resource.transferSize);
335
336         this._refreshCompressed();
337     }
338
339     _refreshRequestHeaders()
340     {
341         if (!this._resource)
342             return;
343
344         this._requestHeadersRow.dataGrid = this._createNameValueDataGrid(this._resource.requestHeaders);
345     }
346
347     _refreshResponseHeaders()
348     {
349         if (!this._resource)
350             return;
351
352         this._responseHeadersRow.dataGrid = this._createNameValueDataGrid(this._resource.responseHeaders);
353     }
354
355     _createNameValueDataGrid(data)
356     {
357         if (!data || data instanceof Array ? !data.length : isEmptyObject(data))
358             return null;
359
360         var dataGrid = new WebInspector.DataGrid({
361             name: {title: WebInspector.UIString("Name"), width: "30%", sortable: true},
362             value: {title: WebInspector.UIString("Value"), sortable: true}
363         });
364
365         function addDataGridNode(nodeValue)
366         {
367             console.assert(typeof nodeValue.name === "string");
368             console.assert(!nodeValue.value || typeof nodeValue.value === "string");
369
370             var node = new WebInspector.DataGridNode({name: nodeValue.name, value: nodeValue.value || ""}, false);
371             dataGrid.appendChild(node);
372         }
373
374         if (data instanceof Array) {
375             for (var i = 0; i < data.length; ++i)
376                 addDataGridNode(data[i]);
377         } else {
378             for (var name in data)
379                 addDataGridNode({name, value: data[name] || ""});
380         }
381
382         dataGrid.addEventListener(WebInspector.DataGrid.Event.SortChanged, sortDataGrid, this);
383
384         function sortDataGrid()
385         {
386             var sortColumnIdentifier = dataGrid.sortColumnIdentifier;
387
388             function comparator(a, b)
389             {
390                 var item1 = a.data[sortColumnIdentifier];
391                 var item2 = b.data[sortColumnIdentifier];
392                 return item1.localeCompare(item2);
393             }
394
395             dataGrid.sortNodes(comparator);
396         }
397
398         return dataGrid;
399     }
400
401     _refreshImageSizeSection()
402     {
403         var resource = this._resource;
404
405         if (!resource)
406             return;
407
408         // Hide the section if we're not dealing with an image or if the load failed.
409         if (resource.type !== WebInspector.Resource.Type.Image || resource.failed) {
410             var imageSectionElement = this._imageSizeSection.element;
411             if (imageSectionElement.parentNode)
412                 this.contentView.element.removeChild(imageSectionElement);
413             return;
414         }
415
416         // Ensure the section is displayed, right before the "Location" section.
417         this.contentView.element.insertBefore(this._imageSizeSection.element, this._locationSection.element);
418
419         // Get the metrics for this resource and fill in the metrics rows with that information.
420         resource.getImageSize(function(size) {
421             this._imageWidthRow.value = WebInspector.UIString("%fpx").format(size.width);
422             this._imageHeightRow.value = WebInspector.UIString("%fpx").format(size.height);
423         }.bind(this));
424     }
425
426     _goToRequestDataClicked()
427     {
428         WebInspector.showResourceRequest(this._resource);
429     }
430
431     _refreshRequestDataSection()
432     {
433         var resource = this._resource;
434
435         if (!resource)
436             return;
437
438         // Hide the section if we're not dealing with a request with data.
439         var requestData = resource.requestData;
440         if (!requestData) {
441             this._requestDataSection.element.remove();
442             return;
443         }
444
445         // Ensure the section is displayed, right before the "Request Headers" section.
446         this.contentView.element.insertBefore(this._requestDataSection.element, this._requestHeadersSection.element);
447
448         var requestDataContentType = resource.requestDataContentType || "";
449         if (requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) {
450             // Simple form data that should be parsable like a query string.
451             var parametersRow = new WebInspector.DetailsSectionDataGridRow(null, WebInspector.UIString("No Parameters"));
452             parametersRow.dataGrid = this._createNameValueDataGrid(parseQueryString(requestData, true));
453
454             this._requestDataSection.groups = [new WebInspector.DetailsSectionGroup([parametersRow])];
455             return;
456         }
457
458         // Not simple form data, so we can really only show the size and type here.
459         // FIXME: Add a go-to arrow here to show the data in the content browser.
460
461         var mimeTypeComponents = parseMIMEType(requestDataContentType);
462
463         var mimeType = mimeTypeComponents.type;
464         var boundary = mimeTypeComponents.boundary;
465         var encoding = mimeTypeComponents.encoding;
466
467         var rows = [];
468
469         var mimeTypeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("MIME Type"));
470         mimeTypeRow.value = mimeType;
471         rows.push(mimeTypeRow);
472
473         if (boundary) {
474             var boundryRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Boundary"));
475             boundryRow.value = boundary;
476             rows.push(boundryRow);
477         }
478
479         if (encoding) {
480             var encodingRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Encoding"));
481             encodingRow.value = encoding;
482             rows.push(encodingRow);
483         }
484
485         var sizeValue = Number.bytesToString(requestData.length);
486
487         var dataValue = document.createDocumentFragment();
488
489         dataValue.append(sizeValue);
490
491         var goToButton = dataValue.appendChild(WebInspector.createGoToArrowButton());
492         goToButton.addEventListener("click", this._goToRequestDataClicked.bind(this));
493
494         var dataRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Data"));
495         dataRow.value = dataValue;
496         rows.push(dataRow);
497
498         this._requestDataSection.groups = [new WebInspector.DetailsSectionGroup(rows)];
499     }
500 };