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.CookieStorageContentView = function(representedObject)
27{
28    WebInspector.ContentView.call(this, representedObject);
29
30    this.element.classList.add(WebInspector.CookieStorageContentView.StyleClassName);
31
32    this.update();
33};
34
35WebInspector.CookieStorageContentView.StyleClassName = "cookie-storage";
36
37WebInspector.CookieStorageContentView.prototype = {
38    constructor: WebInspector.CookieStorageContentView,
39
40    // Public
41
42    update: function()
43    {
44        function callback(error, cookies)
45        {
46            if (error)
47                return;
48
49            this._cookies = this._filterCookies(cookies);
50            this._rebuildTable();
51        }
52
53        PageAgent.getCookies(callback.bind(this));
54    },
55
56    updateLayout: function()
57    {
58        if (this._dataGrid)
59            this._dataGrid.updateLayout();
60    },
61
62    saveToCookie: function(cookie)
63    {
64        cookie.type = WebInspector.ContentViewCookieType.CookieStorage;
65        cookie.host = this.representedObject.host;
66    },
67
68    get scrollableElements()
69    {
70        if (!this._dataGrid)
71            return [];
72        return [this._dataGrid.scrollContainer];
73    },
74
75    // Private
76
77    _rebuildTable: function()
78    {
79        // FIXME: If there are no cookies, do we want to show an empty datagrid, or do something like the old
80        // inspector and show some text saying there are no cookies?
81        if (!this._dataGrid) {
82            var columns = {name: {}, value: {}, domain: {}, path: {}, expires: {}, size: {}, http: {}, secure: {}};
83
84            columns.name.title = WebInspector.UIString("Name");
85            columns.name.sortable = true;
86            columns.name.width = "24%";
87
88            columns.value.title = WebInspector.UIString("Value");
89            columns.value.sortable = true;
90            columns.value.width = "34%";
91
92            columns.domain.title = WebInspector.UIString("Domain");
93            columns.domain.sortable = true;
94            columns.domain.width = "7%";
95
96            columns.path.title = WebInspector.UIString("Path");
97            columns.path.sortable = true;
98            columns.path.width = "7%";
99
100            columns.expires.title = WebInspector.UIString("Expires");
101            columns.expires.sortable = true;
102            columns.expires.width = "7%";
103
104            columns.size.title = WebInspector.UIString("Size");
105            columns.size.aligned = "right";
106            columns.size.sortable = true;
107            columns.size.width = "7%";
108
109            columns.http.title = WebInspector.UIString("HTTP");
110            columns.http.aligned = "centered";
111            columns.http.sortable = true;
112            columns.http.width = "7%";
113
114            columns.secure.title = WebInspector.UIString("Secure");
115            columns.secure.aligned = "centered";
116            columns.secure.sortable = true;
117            columns.secure.width = "7%";
118
119            this._dataGrid = new WebInspector.DataGrid(columns, null, this._deleteCallback.bind(this));
120            this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SortChanged, this._sortDataGrid, this);
121
122            this.element.appendChild(this._dataGrid.element);
123            this._dataGrid.updateLayout();
124        }
125
126        console.assert(this._dataGrid);
127        this._dataGrid.removeChildren();
128
129        for (var cookie of this._cookies) {
130            const checkmark = "\u2713";
131            var data = {
132                "name": cookie.name,
133                "value": cookie.value,
134                "domain": cookie.domain || "",
135                "path": cookie.path || "",
136                "expires": "",
137                "size": Number.bytesToString(cookie.size),
138                "http": cookie.httpOnly ? checkmark : "",
139                "secure": cookie.secure ? checkmark : "",
140            };
141
142            if (cookie.type !== WebInspector.CookieType.Request)
143                data["expires"] = cookie.session ? WebInspector.UIString("Session") : new Date(cookie.expires).toLocaleString();
144
145            var node = new WebInspector.DataGridNode(data);
146            node.selectable = true;
147            node.cookie = cookie;
148
149            this._dataGrid.appendChild(node);
150        }
151
152        this._dataGrid.sortColumnIdentifier = "name";
153    },
154
155    _filterCookies: function(cookies)
156    {
157        var filteredCookies = [];
158        var resourcesForDomain = [];
159
160        var frames = WebInspector.frameResourceManager.frames;
161        for (var i = 0; i < frames.length; ++i) {
162            var resources = frames[i].resources;
163            for (var j = 0; j < resources.length; ++j) {
164                var urlComponents = resources[j].urlComponents;
165                if (urlComponents && urlComponents.host && urlComponents.host === this.representedObject.host)
166                    resourcesForDomain.push(resources[j].url);
167            }
168
169            // The main resource isn't always in the list of resources, make sure to add it to the list of resources
170            // we get the URLs from.
171            var mainResourceURLComponents = frames[i].mainResource.urlComponents;
172            if (mainResourceURLComponents && mainResourceURLComponents.host && mainResourceURLComponents.host == this.representedObject.host)
173                resourcesForDomain.push(frames[i].mainResource.url);
174        }
175
176        for (var i = 0; i < cookies.length; ++i) {
177            for (var j = 0; j < resourcesForDomain.length; ++j) {
178                if (WebInspector.cookieMatchesResourceURL(cookies[i], resourcesForDomain[j])) {
179                    filteredCookies.push(cookies[i]);
180                    break;
181                }
182            }
183        }
184
185        return filteredCookies;
186    },
187
188    _sortDataGrid: function()
189    {
190        function localeCompare(field, nodeA, nodeB)
191        {
192            return (nodeA.data[field] + "").localeCompare(nodeB.data[field] + "");
193        }
194
195        function numberCompare(field, nodeA, nodeB)
196        {
197            return nodeA.cookie[field] - nodeB.cookie[field];
198        }
199
200        function expiresCompare(nodeA, nodeB)
201        {
202            if (nodeA.cookie.session !== nodeB.cookie.session)
203                return nodeA.cookie.session ? 1 : -1;
204
205            if (nodeA.cookie.session)
206                return 0;
207
208            return nodeA.data["expires"] - nodeB.data["expires"];
209        }
210
211        var comparator;
212        switch (this._dataGrid.sortColumnIdentifier) {
213            case "value": comparator = localeCompare.bind(this, "value"); break;
214            case "domain": comparator = localeCompare.bind(this, "domain"); break;
215            case "path": comparator = localeCompare.bind(this, "path"); break;
216            case "expires": comparator = expiresCompare; break;
217            case "size": comparator = numberCompare.bind(this, "size"); break;
218            case "http": comparator = localeCompare.bind(this, "http"); break;
219            case "secure": comparator = localeCompare.bind(this, "secure"); break;
220            case "name":
221            default: comparator = localeCompare.bind(this, "name"); break;
222        }
223
224        console.assert(comparator);
225        this._dataGrid.sortNodes(comparator);
226    },
227
228    _deleteCallback: function(node)
229    {
230        if (!node || !node.cookie)
231            return;
232
233        var cookie = node.cookie;
234        var cookieURL = (cookie.secure ? "https://" : "http://") + cookie.domain + cookie.path;
235
236        // COMPATIBILITY (iOS 6): PageAgent.deleteCookie used to take 'domain', now takes 'url'. Send both.
237        PageAgent.deleteCookie.invoke({cookieName: cookie.name, domain: cookie.domain, url: cookieURL});
238
239        this.update();
240    }
241};
242
243WebInspector.CookieStorageContentView.prototype.__proto__ = WebInspector.ContentView.prototype;
244
245WebInspector.cookieMatchesResourceURL = function(cookie, resourceURL)
246{
247    var parsedURL = parseURL(resourceURL);
248    if (!parsedURL || !WebInspector.cookieDomainMatchesResourceDomain(cookie.domain, parsedURL.host))
249        return false;
250
251    return (parsedURL.path.startsWith(cookie.path)
252        && (!cookie.port || parsedURL.port == cookie.port)
253        && (!cookie.secure || parsedURL.scheme === "https"));
254}
255
256WebInspector.cookieDomainMatchesResourceDomain = function(cookieDomain, resourceDomain)
257{
258    if (cookieDomain.charAt(0) !== ".")
259        return resourceDomain === cookieDomain;
260    return !!resourceDomain.match(new RegExp("^([^\\.]+\\.)?" + cookieDomain.substring(1).escapeForRegExp() + "$"), "i");
261}
262
263WebInspector.CookieType = {
264    Request: 0,
265    Response: 1
266};
267