blob: 2e2c81668a8a95f02f7b28cdd743e9e075b1e159 [file] [log] [blame]
/*
* Copyright (C) 2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WI.CookiePopover = class CookiePopover extends WI.Popover
{
constructor(delegate)
{
super(delegate);
this._nameInputElement = null;
this._valueInputElement = null;
this._domainInputElement = null;
this._pathInputElement = null;
this._sessionCheckboxElement = null;
this._expiresInputElement = null;
this._httpOnlyCheckboxElement = null;
this._secureCheckboxElement = null;
this._sameSiteSelectElement = null;
this._serializedDataWhenShown = null;
this.windowResizeHandler = this._presentOverTargetElement.bind(this);
}
// Public
get serializedData()
{
if (!this._targetElement)
return null;
let name = this._nameInputElement.value;
if (!name)
return null;
let domain = this._domainInputElement.value || this._domainInputElement.placeholder;
if (!domain)
return null;
let path = this._pathInputElement.value || this._pathInputElement.placeholder;
if (!path)
return null;
let session = this._sessionCheckboxElement.checked;
let expires = this._parseExpires();
if (!session && isNaN(expires))
return null;
// If a full URL is entered in the domain input, parse it to get just the domain.
try {
let url = new URL(domain);
domain = url.hostname;
} catch { }
if (!path.startsWith("/"))
path = "/" + path;
let data = {
name,
value: this._valueInputElement.value,
domain,
path,
httpOnly: this._httpOnlyCheckboxElement.checked,
secure: this._secureCheckboxElement.checked,
sameSite: this._sameSiteSelectElement.value,
};
if (session)
data.session = true;
else
data.expires = expires;
if (JSON.stringify(data) === JSON.stringify(this._serializedDataWhenShown))
return null;
return data;
}
show(cookie, targetElement, preferredEdges, options = {})
{
console.assert(!cookie || cookie instanceof WI.Cookie, cookie);
console.assert(targetElement instanceof Element, targetElement);
console.assert(Array.isArray(preferredEdges), preferredEdges);
this._targetElement = targetElement;
this._preferredEdges = preferredEdges;
function formatDate(date) {
function pad(number) {
return String(number).padStart(2, "0");
}
return [
date.getFullYear(),
"-",
pad(date.getMonth() + 1),
"-",
pad(date.getDate()),
"T",
pad(date.getHours()),
":",
pad(date.getMinutes()),
":",
pad(date.getSeconds()),
].join("");
}
let data = {};
if (cookie) {
data.name = cookie.name;
data.value = cookie.value;
data.domain = cookie.domain;
data.path = cookie.path;
data.expires = formatDate(cookie.expires || this._defaultExpires());
data.session = cookie.session;
data.httpOnly = cookie.httpOnly;
data.secure = cookie.secure;
data.sameSite = cookie.sameSite;
} else {
let urlComponents = WI.networkManager.mainFrame.mainResource.urlComponents;
data.name = "";
data.value = "";
data.domain = urlComponents.host;
data.path = urlComponents.path;
data.expires = formatDate(this._defaultExpires());
data.session = true;
data.httpOnly = false;
data.secure = false;
data.sameSite = WI.Cookie.SameSiteType.None;
}
let popoverContentElement = document.createElement("div");
popoverContentElement.className = "cookie-popover-content";
let tableElement = popoverContentElement.appendChild(document.createElement("table"));
function createRow(id, label, editorElement) {
let domId = `cookie-popover-${id}-editor`;
let rowElement = tableElement.appendChild(document.createElement("tr"));
let headerElement = rowElement.appendChild(document.createElement("th"));
let labelElement = headerElement.appendChild(document.createElement("label"));
labelElement.setAttribute("for", domId);
labelElement.textContent = label;
let dataElement = rowElement.appendChild(document.createElement("td"));
editorElement.id = domId;
dataElement.appendChild(editorElement);
if (id === options.focusField) {
setTimeout(() => {
editorElement.focus();
editorElement.select?.();
});
}
return {rowElement};
}
let boundHandleInputKeyDown = this._handleInputKeyDown.bind(this);
function createInputRow(id, label, type, value) {
let inputElement = document.createElement("input");
inputElement.type = type;
if (type === "checkbox")
inputElement.checked = value;
else {
if (cookie)
inputElement.value = value;
inputElement.placeholder = value;
inputElement.spellcheck = false;
inputElement.addEventListener("keydown", boundHandleInputKeyDown);
}
let rowElement = createRow(id, label, inputElement).rowElement;
return {inputElement, rowElement};
}
this._nameInputElement = createInputRow("name", WI.UIString("Name"), "text", data.name).inputElement;
this._nameInputElement.required = true;
this._valueInputElement = createInputRow("value", WI.UIString("Value"), "text", data.value).inputElement;
this._domainInputElement = createInputRow("domain", WI.unlocalizedString("Domain"), "text", data.domain).inputElement;
this._pathInputElement = createInputRow("path", WI.unlocalizedString("Path"), "text", data.path).inputElement;
this._sessionCheckboxElement = createInputRow("session", WI.unlocalizedString("Session"), "checkbox", data.session).inputElement;
let expiresInputRow = createInputRow("expires", WI.unlocalizedString("Expires"), "datetime-local", data.expires);
this._expiresInputElement = expiresInputRow.inputElement;
this._expiresInputElement.step = 1; // Causes the seconds field to be shown.
this._expiresInputElement.addEventListener("input", (event) => {
this._expiresInputElement.classList.toggle("invalid", isNaN(this._parseExpires()));
});
this._httpOnlyCheckboxElement = createInputRow("http-only", WI.unlocalizedString("HttpOnly"), "checkbox", data.httpOnly).inputElement;
this._secureCheckboxElement = createInputRow("secure", WI.unlocalizedString("Secure"), "checkbox", data.secure).inputElement;
this._sameSiteSelectElement = document.createElement("select");
for (let sameSiteType of Object.values(WI.Cookie.SameSiteType)) {
let optionElement = this._sameSiteSelectElement.appendChild(document.createElement("option"));
optionElement.value = sameSiteType;
optionElement.textContent = WI.Cookie.displayNameForSameSiteType(sameSiteType);
}
this._sameSiteSelectElement.value = data.sameSite;
createRow("same-site", WI.unlocalizedString("SameSite"), this._sameSiteSelectElement);
let toggleExpiresRow = () => {
expiresInputRow.rowElement.hidden = this._sessionCheckboxElement.checked;
this.update();
};
this._sessionCheckboxElement.addEventListener("change", (event) => {
toggleExpiresRow();
});
toggleExpiresRow();
this._serializedDataWhenShown = this.serializedData;
this.content = popoverContentElement;
this._presentOverTargetElement();
}
// Private
_presentOverTargetElement()
{
if (!this._targetElement)
return;
let targetFrame = WI.Rect.rectFromClientRect(this._targetElement.getBoundingClientRect());
this.present(targetFrame.pad(2), this._preferredEdges);
}
_defaultExpires()
{
return new Date(Date.now() + (1000 * 60 * 60 * 24)); // one day in the future
}
_parseExpires()
{
let timestamp = Date.parse(this._expiresInputElement.value || this._expiresInputElement.placeholder);
if (timestamp < Date.now())
return NaN;
return timestamp;
}
_handleInputKeyDown(event)
{
if (event.key === "Enter" || event.key === "Esc")
this.dismiss();
}
};