1/*
2 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3 * Copyright (C) IBM Corp. 2009  All rights reserved.
4 * Copyright (C) 2010 Google Inc. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1.  Redistributions of source code must retain the above copyright
11 *     notice, this list of conditions and the following disclaimer.
12 * 2.  Redistributions in binary form must reproduce the above copyright
13 *     notice, this list of conditions and the following disclaimer in the
14 *     documentation and/or other materials provided with the distribution.
15 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16 *     its contributors may be used to endorse or promote products derived
17 *     from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @constructor
33 * @extends {WebInspector.VBox}
34 * @param {!WebInspector.NetworkRequest} request
35 */
36WebInspector.RequestHeadersView = function(request)
37{
38    WebInspector.VBox.call(this);
39    this.registerRequiredCSS("resourceView.css");
40    this.registerRequiredCSS("requestHeadersView.css");
41    this.element.classList.add("request-headers-view");
42
43    this._request = request;
44
45    this._headersListElement = document.createElement("ol");
46    this._headersListElement.className = "outline-disclosure";
47    this.element.appendChild(this._headersListElement);
48
49    this._headersTreeOutline = new TreeOutline(this._headersListElement);
50    this._headersTreeOutline.expandTreeElementsWhenArrowing = true;
51
52    this._remoteAddressTreeElement = new TreeElement("", null, false);
53    this._remoteAddressTreeElement.selectable = false;
54    this._remoteAddressTreeElement.hidden = true;
55    this._headersTreeOutline.appendChild(this._remoteAddressTreeElement);
56
57    this._urlTreeElement = new TreeElement("", null, false);
58    this._urlTreeElement.selectable = false;
59    this._headersTreeOutline.appendChild(this._urlTreeElement);
60
61    this._requestMethodTreeElement = new TreeElement("", null, false);
62    this._requestMethodTreeElement.selectable = false;
63    this._headersTreeOutline.appendChild(this._requestMethodTreeElement);
64
65    this._statusCodeTreeElement = new TreeElement("", null, false);
66    this._statusCodeTreeElement.selectable = false;
67    this._headersTreeOutline.appendChild(this._statusCodeTreeElement);
68
69    this._requestHeadersTreeElement = new TreeElement("", null, true);
70    this._requestHeadersTreeElement.expanded = true;
71    this._requestHeadersTreeElement.selectable = false;
72    this._headersTreeOutline.appendChild(this._requestHeadersTreeElement);
73
74    this._decodeRequestParameters = true;
75
76    this._showRequestHeadersText = false;
77    this._showResponseHeadersText = false;
78
79    this._queryStringTreeElement = new TreeElement("", null, true);
80    this._queryStringTreeElement.expanded = true;
81    this._queryStringTreeElement.selectable = false;
82    this._queryStringTreeElement.hidden = true;
83    this._headersTreeOutline.appendChild(this._queryStringTreeElement);
84
85    this._formDataTreeElement = new TreeElement("", null, true);
86    this._formDataTreeElement.expanded = true;
87    this._formDataTreeElement.selectable = false;
88    this._formDataTreeElement.hidden = true;
89    this._headersTreeOutline.appendChild(this._formDataTreeElement);
90
91    this._requestPayloadTreeElement = new TreeElement(WebInspector.UIString("Request Payload"), null, true);
92    this._requestPayloadTreeElement.expanded = true;
93    this._requestPayloadTreeElement.selectable = false;
94    this._requestPayloadTreeElement.hidden = true;
95    this._headersTreeOutline.appendChild(this._requestPayloadTreeElement);
96
97    this._responseHeadersTreeElement = new TreeElement("", null, true);
98    this._responseHeadersTreeElement.expanded = true;
99    this._responseHeadersTreeElement.selectable = false;
100    this._headersTreeOutline.appendChild(this._responseHeadersTreeElement);
101}
102
103WebInspector.RequestHeadersView.prototype = {
104
105    wasShown: function()
106    {
107        this._request.addEventListener(WebInspector.NetworkRequest.Events.RemoteAddressChanged, this._refreshRemoteAddress, this);
108        this._request.addEventListener(WebInspector.NetworkRequest.Events.RequestHeadersChanged, this._refreshRequestHeaders, this);
109        this._request.addEventListener(WebInspector.NetworkRequest.Events.ResponseHeadersChanged, this._refreshResponseHeaders, this);
110        this._request.addEventListener(WebInspector.NetworkRequest.Events.FinishedLoading, this._refreshHTTPInformation, this);
111
112        this._refreshURL();
113        this._refreshQueryString();
114        this._refreshRequestHeaders();
115        this._refreshResponseHeaders();
116        this._refreshHTTPInformation();
117        this._refreshRemoteAddress();
118    },
119
120    willHide: function()
121    {
122        this._request.removeEventListener(WebInspector.NetworkRequest.Events.RemoteAddressChanged, this._refreshRemoteAddress, this);
123        this._request.removeEventListener(WebInspector.NetworkRequest.Events.RequestHeadersChanged, this._refreshRequestHeaders, this);
124        this._request.removeEventListener(WebInspector.NetworkRequest.Events.ResponseHeadersChanged, this._refreshResponseHeaders, this);
125        this._request.removeEventListener(WebInspector.NetworkRequest.Events.FinishedLoading, this._refreshHTTPInformation, this);
126    },
127
128    /**
129     * @param {string} name
130     * @param {string} value
131     * @return {!DocumentFragment}
132     */
133    _formatHeader: function(name, value)
134    {
135        var fragment = document.createDocumentFragment();
136        fragment.createChild("div", "header-name").textContent = name + ":";
137        fragment.createChild("div", "header-value source-code").textContent = value;
138
139        return fragment;
140    },
141
142    /**
143     * @param {string} value
144     * @param {string} className
145     * @param {boolean} decodeParameters
146     * @return {!Element}
147     */
148    _formatParameter: function(value, className, decodeParameters)
149    {
150        var errorDecoding = false;
151
152        if (decodeParameters) {
153            value = value.replace(/\+/g, " ");
154            if (value.indexOf("%") >= 0) {
155                try {
156                    value = decodeURIComponent(value);
157                } catch (e) {
158                    errorDecoding = true;
159                }
160            }
161        }
162        var div = document.createElement("div");
163        div.className = className;
164        if (errorDecoding)
165            div.createChild("span", "error-message").textContent = WebInspector.UIString("(unable to decode value)");
166        else
167            div.textContent = value;
168        return div;
169    },
170
171    _refreshURL: function()
172    {
173        this._urlTreeElement.title = this._formatHeader(WebInspector.UIString("Request URL"), this._request.url);
174    },
175
176    _refreshQueryString: function()
177    {
178        var queryString = this._request.queryString();
179        var queryParameters = this._request.queryParameters;
180        this._queryStringTreeElement.hidden = !queryParameters;
181        if (queryParameters)
182            this._refreshParams(WebInspector.UIString("Query String Parameters"), queryParameters, queryString, this._queryStringTreeElement);
183    },
184
185    _refreshFormData: function()
186    {
187        this._formDataTreeElement.hidden = true;
188        this._requestPayloadTreeElement.hidden = true;
189
190        var formData = this._request.requestFormData;
191        if (!formData)
192            return;
193
194        var formParameters = this._request.formParameters;
195        if (formParameters) {
196            this._formDataTreeElement.hidden = false;
197            this._refreshParams(WebInspector.UIString("Form Data"), formParameters, formData, this._formDataTreeElement);
198        } else {
199            this._requestPayloadTreeElement.hidden = false;
200            try {
201                var json = JSON.parse(formData);
202                this._refreshRequestJSONPayload(json, formData);
203            } catch (e) {
204                this._populateTreeElementWithSourceText(this._requestPayloadTreeElement, formData);
205            }
206        }
207    },
208
209    /**
210     * @param {!TreeElement} treeElement
211     * @param {?string} sourceText
212     */
213    _populateTreeElementWithSourceText: function(treeElement, sourceText)
214    {
215        var sourceTextElement = document.createElement("span");
216        sourceTextElement.classList.add("header-value");
217        sourceTextElement.classList.add("source-code");
218        sourceTextElement.textContent = String(sourceText || "").trim();
219
220        var sourceTreeElement = new TreeElement(sourceTextElement);
221        sourceTreeElement.selectable = false;
222        treeElement.removeChildren();
223        treeElement.appendChild(sourceTreeElement);
224    },
225
226    /**
227     * @param {string} title
228     * @param {?Array.<!WebInspector.NetworkRequest.NameValue>} params
229     * @param {?string} sourceText
230     * @param {!TreeElement} paramsTreeElement
231     */
232    _refreshParams: function(title, params, sourceText, paramsTreeElement)
233    {
234        paramsTreeElement.removeChildren();
235
236        paramsTreeElement.listItemElement.removeChildren();
237        paramsTreeElement.listItemElement.createTextChild(title);
238
239        var headerCount = document.createElement("span");
240        headerCount.classList.add("header-count");
241        headerCount.textContent = WebInspector.UIString(" (%d)", params.length);
242        paramsTreeElement.listItemElement.appendChild(headerCount);
243
244        /**
245         * @param {!Event} event
246         * @this {WebInspector.RequestHeadersView}
247         */
248        function toggleViewSource(event)
249        {
250            paramsTreeElement._viewSource = !paramsTreeElement._viewSource;
251            this._refreshParams(title, params, sourceText, paramsTreeElement);
252        }
253
254        paramsTreeElement.listItemElement.appendChild(this._createViewSourceToggle(paramsTreeElement._viewSource, toggleViewSource.bind(this)));
255
256        if (paramsTreeElement._viewSource) {
257            this._populateTreeElementWithSourceText(paramsTreeElement, sourceText);
258            return;
259        }
260
261        var toggleTitle = this._decodeRequestParameters ? WebInspector.UIString("view URL encoded") : WebInspector.UIString("view decoded");
262        var toggleButton = this._createToggleButton(toggleTitle);
263        toggleButton.addEventListener("click", this._toggleURLDecoding.bind(this), false);
264        paramsTreeElement.listItemElement.appendChild(toggleButton);
265
266        for (var i = 0; i < params.length; ++i) {
267            var paramNameValue = document.createDocumentFragment();
268            var name = this._formatParameter(params[i].name + ":", "header-name", this._decodeRequestParameters);
269            var value = this._formatParameter(params[i].value, "header-value source-code", this._decodeRequestParameters);
270            paramNameValue.appendChild(name);
271            paramNameValue.appendChild(value);
272
273            var parmTreeElement = new TreeElement(paramNameValue, null, false);
274            parmTreeElement.selectable = false;
275            paramsTreeElement.appendChild(parmTreeElement);
276        }
277    },
278
279    /**
280     * @param {*} parsedObject
281     * @param {string} sourceText
282     */
283    _refreshRequestJSONPayload: function(parsedObject, sourceText)
284    {
285        var treeElement = this._requestPayloadTreeElement;
286        treeElement.removeChildren();
287
288        var listItem = this._requestPayloadTreeElement.listItemElement;
289        listItem.removeChildren();
290        listItem.createTextChild(this._requestPayloadTreeElement.title);
291
292        /**
293         * @param {!Event} event
294         * @this {WebInspector.RequestHeadersView}
295         */
296        function toggleViewSource(event)
297        {
298            treeElement._viewSource = !treeElement._viewSource;
299            this._refreshRequestJSONPayload(parsedObject, sourceText);
300        }
301
302        listItem.appendChild(this._createViewSourceToggle(treeElement._viewSource, toggleViewSource.bind(this)));
303        if (treeElement._viewSource) {
304            this._populateTreeElementWithSourceText(this._requestPayloadTreeElement, sourceText);
305        } else {
306            var object = WebInspector.RemoteObject.fromLocalObject(parsedObject);
307            var section = new WebInspector.ObjectPropertiesSection(object, object.description);
308            section.expand();
309            section.editable = false;
310            listItem.appendChild(section.element);
311        }
312    },
313
314    /**
315     * @param {boolean} viewSource
316     * @param {function(!Event)} handler
317     * @return {!Element}
318     */
319    _createViewSourceToggle: function(viewSource, handler)
320    {
321        var viewSourceToggleTitle = viewSource ? WebInspector.UIString("view parsed") : WebInspector.UIString("view source");
322        var viewSourceToggleButton = this._createToggleButton(viewSourceToggleTitle);
323        viewSourceToggleButton.addEventListener("click", handler, false);
324        return viewSourceToggleButton;
325    },
326
327    /**
328     * @param {!Event} event
329     */
330    _toggleURLDecoding: function(event)
331    {
332        this._decodeRequestParameters = !this._decodeRequestParameters;
333        this._refreshQueryString();
334        this._refreshFormData();
335    },
336
337    _refreshRequestHeaders: function()
338    {
339        var treeElement = this._requestHeadersTreeElement;
340
341        var headers = this._request.requestHeaders();
342        headers = headers.slice();
343        headers.sort(function(a, b) { return a.name.toLowerCase().compareTo(b.name.toLowerCase()) });
344        var headersText = this._request.requestHeadersText();
345
346        if (this._showRequestHeadersText && headersText)
347            this._refreshHeadersText(WebInspector.UIString("Request Headers"), headers.length, headersText, treeElement);
348        else
349            this._refreshHeaders(WebInspector.UIString("Request Headers"), headers, treeElement, headersText === undefined);
350
351        if (headersText) {
352            var toggleButton = this._createHeadersToggleButton(this._showRequestHeadersText);
353            toggleButton.addEventListener("click", this._toggleRequestHeadersText.bind(this), false);
354            treeElement.listItemElement.appendChild(toggleButton);
355        }
356
357        this._refreshFormData();
358    },
359
360    _refreshResponseHeaders: function()
361    {
362        var treeElement = this._responseHeadersTreeElement;
363        var headers = this._request.sortedResponseHeaders;
364        var headersText = this._request.responseHeadersText;
365
366        if (this._showResponseHeadersText)
367            this._refreshHeadersText(WebInspector.UIString("Response Headers"), headers.length, headersText, treeElement);
368        else
369            this._refreshHeaders(WebInspector.UIString("Response Headers"), headers, treeElement);
370
371        if (headersText) {
372            var toggleButton = this._createHeadersToggleButton(this._showResponseHeadersText);
373            toggleButton.addEventListener("click", this._toggleResponseHeadersText.bind(this), false);
374            treeElement.listItemElement.appendChild(toggleButton);
375        }
376    },
377
378    _refreshHTTPInformation: function()
379    {
380        var requestMethodElement = this._requestMethodTreeElement;
381        requestMethodElement.hidden = !this._request.statusCode;
382        var statusCodeElement = this._statusCodeTreeElement;
383        statusCodeElement.hidden = !this._request.statusCode;
384
385        if (this._request.statusCode) {
386            var statusCodeFragment = document.createDocumentFragment();
387            statusCodeFragment.createChild("div", "header-name").textContent = WebInspector.UIString("Status Code") + ":";
388
389            var statusCodeImage = statusCodeFragment.createChild("div", "resource-status-image");
390            statusCodeImage.title = this._request.statusCode + " " + this._request.statusText;
391
392            if (this._request.statusCode < 300 || this._request.statusCode === 304)
393                statusCodeImage.classList.add("green-ball");
394            else if (this._request.statusCode < 400)
395                statusCodeImage.classList.add("orange-ball");
396            else
397                statusCodeImage.classList.add("red-ball");
398
399            requestMethodElement.title = this._formatHeader(WebInspector.UIString("Request Method"), this._request.requestMethod);
400
401            var statusTextElement = statusCodeFragment.createChild("div", "header-value source-code");
402            var statusText = this._request.statusCode + " " + this._request.statusText;
403            if (this._request.fetchedViaServiceWorker) {
404                statusText += " " + WebInspector.UIString("(from ServiceWorker)");
405                statusTextElement.classList.add("status-from-cache");
406            } else if (this._request.cached) {
407                statusText += " " + WebInspector.UIString("(from cache)");
408                statusTextElement.classList.add("status-from-cache");
409            }
410            statusTextElement.textContent = statusText;
411
412            statusCodeElement.title = statusCodeFragment;
413        }
414    },
415
416    /**
417     * @param {string} title
418     * @param {!TreeElement} headersTreeElement
419     * @param {number} headersLength
420     */
421    _refreshHeadersTitle: function(title, headersTreeElement, headersLength)
422    {
423        headersTreeElement.listItemElement.removeChildren();
424        headersTreeElement.listItemElement.createTextChild(title);
425
426        var headerCount = WebInspector.UIString(" (%d)", headersLength);
427        headersTreeElement.listItemElement.createChild("span", "header-count").textContent = headerCount;
428    },
429
430    /**
431     * @param {string} title
432     * @param {!Array.<!WebInspector.NetworkRequest.NameValue>} headers
433     * @param {!TreeElement} headersTreeElement
434     * @param {boolean=} provisionalHeaders
435     */
436    _refreshHeaders: function(title, headers, headersTreeElement, provisionalHeaders)
437    {
438        headersTreeElement.removeChildren();
439
440        var length = headers.length;
441        this._refreshHeadersTitle(title, headersTreeElement, length);
442
443        if (provisionalHeaders) {
444            var cautionText = WebInspector.UIString("Provisional headers are shown");
445            var cautionFragment = document.createDocumentFragment();
446            cautionFragment.createChild("div", "warning-icon-small");
447            cautionFragment.createChild("div", "caution").textContent = cautionText;
448            var cautionTreeElement = new TreeElement(cautionFragment);
449            cautionTreeElement.selectable = false;
450            headersTreeElement.appendChild(cautionTreeElement);
451        }
452
453        headersTreeElement.hidden = !length && !provisionalHeaders;
454        for (var i = 0; i < length; ++i) {
455            var headerTreeElement = new TreeElement(this._formatHeader(headers[i].name, headers[i].value));
456            headerTreeElement.selectable = false;
457            headersTreeElement.appendChild(headerTreeElement);
458        }
459    },
460
461    /**
462     * @param {string} title
463     * @param {number} count
464     * @param {string} headersText
465     * @param {!TreeElement} headersTreeElement
466     */
467    _refreshHeadersText: function(title, count, headersText, headersTreeElement)
468    {
469        this._populateTreeElementWithSourceText(headersTreeElement, headersText);
470        this._refreshHeadersTitle(title, headersTreeElement, count);
471    },
472
473    _refreshRemoteAddress: function()
474    {
475        var remoteAddress = this._request.remoteAddress();
476        var treeElement = this._remoteAddressTreeElement;
477        treeElement.hidden = !remoteAddress;
478        if (remoteAddress)
479            treeElement.title = this._formatHeader(WebInspector.UIString("Remote Address"), remoteAddress);
480    },
481
482    /**
483     * @param {!Event} event
484     */
485    _toggleRequestHeadersText: function(event)
486    {
487        this._showRequestHeadersText = !this._showRequestHeadersText;
488        this._refreshRequestHeaders();
489    },
490
491    /**
492     * @param {!Event} event
493     */
494    _toggleResponseHeadersText: function(event)
495    {
496        this._showResponseHeadersText = !this._showResponseHeadersText;
497        this._refreshResponseHeaders();
498    },
499
500    /**
501     * @param {string} title
502     * @return {!Element}
503     */
504    _createToggleButton: function(title)
505    {
506        var button = document.createElement("span");
507        button.classList.add("header-toggle");
508        button.textContent = title;
509        return button;
510    },
511
512    /**
513     * @param {boolean} isHeadersTextShown
514     * @return {!Element}
515     */
516    _createHeadersToggleButton: function(isHeadersTextShown)
517    {
518        var toggleTitle = isHeadersTextShown ? WebInspector.UIString("view parsed") : WebInspector.UIString("view source");
519        return this._createToggleButton(toggleTitle);
520    },
521
522    __proto__: WebInspector.VBox.prototype
523}
524