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