1/*
2 * Copyright (C) 2020 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
26WI.CookiePopover = class CookiePopover extends WI.Popover
27{
28 constructor(delegate)
29 {
30 super(delegate);
31
32 this._nameInputElement = null;
33 this._valueInputElement = null;
34 this._domainInputElement = null;
35 this._pathInputElement = null;
36 this._sessionCheckboxElement = null;
37 this._expiresInputElement = null;
38 this._httpOnlyCheckboxElement = null;
39 this._secureCheckboxElement = null;
40 this._sameSiteSelectElement = null;
41
42 this._serializedDataWhenShown = null;
43
44 this.windowResizeHandler = this._presentOverTargetElement.bind(this);
45 }
46
47 // Public
48
49 get serializedData()
50 {
51 if (!this._targetElement)
52 return null;
53
54 let name = this._nameInputElement.value || this._nameInputElement.placeholder;
55 if (!name)
56 return null;
57
58 let value = this._valueInputElement.value || this._valueInputElement.placeholder;
59 if (!value)
60 return null;
61
62 let domain = this._domainInputElement.value || this._domainInputElement.placeholder;
63 if (!domain)
64 return null;
65
66 let path = this._pathInputElement.value || this._pathInputElement.placeholder;
67 if (!path)
68 return null;
69
70 let session = this._sessionCheckboxElement.checked;
71 let expires = this._parseExpires();
72 if (!session && isNaN(expires))
73 return null;
74
75 // If a full URL is entered in the domain input, parse it to get just the domain.
76 try {
77 let url = new URL(domain);
78 domain = url.hostname;
79 } catch { }
80
81 if (!path.startsWith("/"))
82 path = "/" + path;
83
84 let data = {
85 name,
86 value,
87 domain,
88 path,
89 httpOnly: this._httpOnlyCheckboxElement.checked,
90 secure: this._secureCheckboxElement.checked,
91 sameSite: this._sameSiteSelectElement.value,
92 };
93
94 if (session)
95 data.session = true;
96 else
97 data.expires = expires;
98
99 if (JSON.stringify(data) === JSON.stringify(this._serializedDataWhenShown))
100 return null;
101
102 return data;
103 }
104
105 show(cookie, targetElement, preferredEdges)
106 {
107 console.assert(!cookie || cookie instanceof WI.Cookie, cookie);
108 console.assert(targetElement instanceof Element, targetElement);
109 console.assert(Array.isArray(preferredEdges), preferredEdges);
110
111 this._targetElement = targetElement;
112 this._preferredEdges = preferredEdges;
113
114 let data = {};
115 if (cookie) {
116 data.name = cookie.name;
117 data.value = cookie.value;
118 data.domain = cookie.domain;
119 data.path = cookie.path;
120 data.expires = (cookie.expires || this._defaultExpires()).toLocaleString();
121 data.session = cookie.session;
122 data.httpOnly = cookie.httpOnly;
123 data.secure = cookie.secure;
124 data.sameSite = cookie.sameSite;
125 } else {
126 let urlComponents = WI.networkManager.mainFrame.mainResource.urlComponents;
127 data.name = WI.unlocalizedString("name");
128 data.value = WI.unlocalizedString("value");
129 data.domain = urlComponents.host;
130 data.path = urlComponents.path;
131 data.expires = this._defaultExpires().toLocaleString();
132 data.session = true;
133 data.httpOnly = false;
134 data.secure = false;
135 data.sameSite = WI.Cookie.SameSiteType.None;
136 }
137
138 let popoverContentElement = document.createElement("div");
139 popoverContentElement.className = "cookie-popover-content";
140
141 let tableElement = popoverContentElement.appendChild(document.createElement("table"));
142
143 function createRow(id, label, editorElement) {
144 id = `cookie-popover-${id}-editor`;
145
146 let rowElement = tableElement.appendChild(document.createElement("tr"));
147
148 let headerElement = rowElement.appendChild(document.createElement("th"));
149
150 let labelElement = headerElement.appendChild(document.createElement("label"));
151 labelElement.setAttribute("for", id);
152 labelElement.textContent = label;
153
154 let dataElement = rowElement.appendChild(document.createElement("td"));
155
156 editorElement.id = id;
157 dataElement.appendChild(editorElement);
158
159 return {rowElement};
160 }
161
162 let boundHandleInputKeyDown = this._handleInputKeyDown.bind(this);
163
164 function createInputRow(id, label, type, value) {
165 let inputElement = document.createElement("input");
166 inputElement.type = type;
167
168 if (type === "checkbox")
169 inputElement.checked = value;
170 else {
171 if (cookie)
172 inputElement.value = value;
173 inputElement.placeholder = value;
174 inputElement.addEventListener("keydown", boundHandleInputKeyDown);
175 }
176
177 let rowElement = createRow(id, label, inputElement).rowElement;
178
179 return {inputElement, rowElement};
180 }
181
182 this._nameInputElement = createInputRow("name", WI.UIString("Name"), "text", data.name).inputElement;
183
184 this._valueInputElement = createInputRow("value", WI.UIString("Value"), "text", data.value).inputElement;
185
186 this._domainInputElement = createInputRow("domain", WI.unlocalizedString("Domain"), "text", data.domain).inputElement;
187
188 this._pathInputElement = createInputRow("path", WI.unlocalizedString("Path"), "text", data.path).inputElement;
189
190 this._sessionCheckboxElement = createInputRow("session", WI.unlocalizedString("Session"), "checkbox", data.session).inputElement;
191
192 let expiresInputRow = createInputRow("expires", WI.unlocalizedString("Expires"), "datetime-local", data.expires);
193 this._expiresInputElement = expiresInputRow.inputElement;
194 this._expiresInputElement.addEventListener("input", (event) => {
195 this._expiresInputElement.classList.toggle("invalid", isNaN(this._parseExpires()));
196 });
197
198 this._httpOnlyCheckboxElement = createInputRow("http-only", WI.unlocalizedString("HttpOnly"), "checkbox", data.httpOnly).inputElement;
199
200 this._secureCheckboxElement = createInputRow("secure", WI.unlocalizedString("Secure"), "checkbox", data.secure).inputElement;
201
202 this._sameSiteSelectElement = document.createElement("select");
203 for (let sameSiteType of Object.values(WI.Cookie.SameSiteType)) {
204 let optionElement = this._sameSiteSelectElement.appendChild(document.createElement("option"));
205 optionElement.textContent = sameSiteType;
206 }
207 createRow("same-site", WI.unlocalizedString("SameSite"), this._sameSiteSelectElement);
208
209 let toggleExpiresRow = () => {
210 expiresInputRow.rowElement.hidden = this._sessionCheckboxElement.checked;
211
212 this.update();
213 };
214
215 this._sessionCheckboxElement.addEventListener("change", (event) => {
216 toggleExpiresRow();
217 });
218
219 toggleExpiresRow();
220
221 this._serializedDataWhenShown = this.serializedData;
222
223 this.content = popoverContentElement;
224 this._presentOverTargetElement();
225 }
226
227 // Private
228
229 _presentOverTargetElement()
230 {
231 if (!this._targetElement)
232 return;
233
234 let targetFrame = WI.Rect.rectFromClientRect(this._targetElement.getBoundingClientRect());
235 this.present(targetFrame.pad(2), this._preferredEdges);
236 }
237
238 _defaultExpires()
239 {
240 return new Date(Date.now() + (1000 * 60 * 60 * 24)); // one day in the future
241 }
242
243 _parseExpires()
244 {
245 let timestamp = Date.parse(this._expiresInputElement.value || this._expiresInputElement.placeholder);
246 if (timestamp < Date.now())
247 return NaN;
248 return timestamp;
249 }
250
251 _handleInputKeyDown(event)
252 {
253 if (event.key === "Enter" || event.key === "Esc")
254 this.dismiss();
255 }
256};