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
26WebInspector.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
109WebInspector.ResourceDetailsSidebarPanel.StyleClassName = "resource";
110
111WebInspector.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(!nodeValue.value || 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 sortColumnIdentifier = dataGrid.sortColumnIdentifier;
363
364            function comparator(a, b)
365            {
366                var item1 = a.data[sortColumnIdentifier];
367                var item2 = b.data[sortColumnIdentifier];
368                return item1.localeCompare(item2);
369            }
370
371            dataGrid.sortNodes(comparator);
372        }
373
374        return dataGrid;
375    },
376
377    _refreshImageSizeSection: function()
378    {
379        var resource = this._resource;
380
381        if (!resource)
382            return;
383
384        // Hide the section if we're not dealing with an image or if the load failed.
385        if (resource.type !== WebInspector.Resource.Type.Image || resource.failed) {
386            var imageSectionElement = this._imageSizeSection.element;
387            if (imageSectionElement.parentNode)
388                this.element.removeChild(imageSectionElement);
389            return;
390        }
391
392        // Ensure the section is displayed, right before the "Location" section.
393        this.element.insertBefore(this._imageSizeSection.element, this._locationSection.element);
394
395        // Get the metrics for this resource and fill in the metrics rows with that information.
396        resource.getImageSize(function(size) {
397            this._imageWidthRow.value = WebInspector.UIString("%fpx").format(size.width);
398            this._imageHeightRow.value = WebInspector.UIString("%fpx").format(size.height);
399        }.bind(this));
400    },
401
402    _goToRequestDataClicked: function()
403    {
404        WebInspector.resourceSidebarPanel.showResourceRequest(this._resource);
405    },
406
407    _refreshRequestDataSection: function()
408    {
409        var resource = this._resource;
410
411        if (!resource)
412            return;
413
414        // Hide the section if we're not dealing with a request with data.
415        var requestData = resource.requestData;
416        if (!requestData) {
417            this._requestDataSection.element.remove();
418            return;
419        }
420
421        // Ensure the section is displayed, right before the "Request Headers" section.
422        this.element.insertBefore(this._requestDataSection.element, this._requestHeadersSection.element);
423
424        var requestDataContentType = resource.requestDataContentType || "";
425        if (requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) {
426            // Simple form data that should be parsable like a query string.
427            var parametersRow = new WebInspector.DetailsSectionDataGridRow(null, WebInspector.UIString("No Parameters"));
428            parametersRow.dataGrid = this._createNameValueDataGrid(parseQueryString(requestData, true));
429
430            this._requestDataSection.groups = [new WebInspector.DetailsSectionGroup([parametersRow])];
431            return;
432        }
433
434        // Not simple form data, so we can really only show the size and type here.
435        // FIXME: Add a go-to arrow here to show the data in the content browser.
436
437        var mimeTypeComponents = parseMIMEType(requestDataContentType);
438
439        var mimeType = mimeTypeComponents.type;
440        var boundary = mimeTypeComponents.boundary;
441        var encoding = mimeTypeComponents.encoding;
442
443        var rows = [];
444
445        var mimeTypeRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("MIME Type"));
446        mimeTypeRow.value = mimeType;
447        rows.push(mimeTypeRow);
448
449        if (boundary) {
450            var boundryRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Boundary"));
451            boundryRow.value = boundary;
452            rows.push(boundryRow);
453        }
454
455        if (encoding) {
456            var encodingRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Encoding"));
457            encodingRow.value = encoding;
458            rows.push(encodingRow);
459        }
460
461        var sizeValue = Number.bytesToString(requestData.length);
462
463        var dataValue = document.createDocumentFragment();
464
465        dataValue.appendChild(document.createTextNode(sizeValue));
466
467        var goToButton = dataValue.appendChild(WebInspector.createGoToArrowButton());
468        goToButton.addEventListener("click", this._goToRequestDataClicked.bind(this));
469
470        var dataRow = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Data"));
471        dataRow.value = dataValue;
472        rows.push(dataRow);
473
474        this._requestDataSection.groups = [new WebInspector.DetailsSectionGroup(rows)];
475    }
476};
477
478WebInspector.ResourceDetailsSidebarPanel.prototype.__proto__ = WebInspector.DetailsSidebarPanel.prototype;
479