635 lines
20 KiB
JavaScript
635 lines
20 KiB
JavaScript
/*
|
|
* Copyright (C) 2013, 2015, 2018 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.CookieStorageContentView = class CookieStorageContentView extends WI.ContentView
|
|
{
|
|
constructor(representedObject)
|
|
{
|
|
super(representedObject);
|
|
|
|
this.element.classList.add("cookie-storage");
|
|
|
|
this._cookies = [];
|
|
this._filteredCookies = [];
|
|
this._sortComparator = null;
|
|
this._table = null;
|
|
this._knownCells = new WeakSet;
|
|
|
|
this._emptyFilterResultsMessageElement = null;
|
|
|
|
this._filterBarNavigationItem = new WI.FilterBarNavigationItem;
|
|
this._filterBarNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._handleFilterBarFilterDidChange, this);
|
|
|
|
if (InspectorBackend.hasCommand("Page.setCookie")) {
|
|
this._setCookieButtonNavigationItem = new WI.ButtonNavigationItem("cookie-storage-set-cookie", WI.UIString("Add Cookie"), "Images/Plus15.svg", 15, 15);
|
|
this._setCookieButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleSetCookieButtonClick, this);
|
|
}
|
|
|
|
if (InspectorBackend.hasCommand("Page.getCookies")) {
|
|
this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("cookie-storage-refresh", WI.UIString("Refresh"), "Images/ReloadFull.svg", 13, 13);
|
|
this._refreshButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._refreshButtonClicked, this);
|
|
}
|
|
|
|
if (InspectorBackend.hasCommand("Page.deleteCookie")) {
|
|
this._clearButtonNavigationItem = new WI.ButtonNavigationItem("cookie-storage-clear", WI.UIString("Clear Cookies"), "Images/NavigationItemTrash.svg", 15, 15);
|
|
this._clearButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
|
|
this._clearButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleClearNavigationItemClicked, this);
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
get navigationItems()
|
|
{
|
|
let navigationItems = [];
|
|
navigationItems.push(this._filterBarNavigationItem);
|
|
navigationItems.push(new WI.DividerNavigationItem);
|
|
if (this._setCookieButtonNavigationItem)
|
|
navigationItems.push(this._setCookieButtonNavigationItem);
|
|
if (this._refreshButtonNavigationItem)
|
|
navigationItems.push(this._refreshButtonNavigationItem);
|
|
if (this._clearButtonNavigationItem)
|
|
navigationItems.push(this._clearButtonNavigationItem);
|
|
return navigationItems;
|
|
}
|
|
|
|
saveToCookie(cookie)
|
|
{
|
|
cookie.type = WI.ContentViewCookieType.CookieStorage;
|
|
cookie.host = this.representedObject.host;
|
|
}
|
|
|
|
get scrollableElements()
|
|
{
|
|
if (!this._table)
|
|
return [];
|
|
return [this._table.scrollContainer];
|
|
}
|
|
|
|
get canFocusFilterBar()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
focusFilterBar()
|
|
{
|
|
this._filterBarNavigationItem.filterBar.focus();
|
|
}
|
|
|
|
handleCopyEvent(event)
|
|
{
|
|
if (!this._table || !this._table.selectedRows.length)
|
|
return;
|
|
|
|
let cookies = this._cookiesAtIndexes(this._table.selectedRows);
|
|
if (!cookies.length)
|
|
return;
|
|
|
|
event.clipboardData.setData("text/plain", this._formatCookiesAsText(cookies));
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
}
|
|
|
|
// Table dataSource
|
|
|
|
tableIndexForRepresentedObject(table, object)
|
|
{
|
|
let index = this._filteredCookies.indexOf(object);
|
|
console.assert(index >= 0);
|
|
return index;
|
|
}
|
|
|
|
tableRepresentedObjectForIndex(table, index)
|
|
{
|
|
console.assert(index >= 0 && index < this._filteredCookies.length);
|
|
return this._filteredCookies[index];
|
|
}
|
|
|
|
tableNumberOfRows(table)
|
|
{
|
|
return this._filteredCookies.length;
|
|
}
|
|
|
|
tableSortChanged(table)
|
|
{
|
|
this._generateSortComparator();
|
|
|
|
if (!this._sortComparator)
|
|
return;
|
|
|
|
this._updateSort();
|
|
this._updateFilteredCookies();
|
|
this._updateEmptyFilterResultsMessage();
|
|
this._table.reloadData();
|
|
}
|
|
|
|
// Table delegate
|
|
|
|
tableCellContextMenuClicked(table, cell, column, rowIndex, event)
|
|
{
|
|
let contextMenu = WI.ContextMenu.createFromEvent(event);
|
|
|
|
contextMenu.appendSeparator();
|
|
|
|
if (InspectorBackend.hasCommand("Page.setCookie") && column.identifier !== "size") {
|
|
contextMenu.appendItem(WI.UIString("Edit %s").format(column.name), () => {
|
|
this._showCookiePopover(cell, this._filteredCookies[rowIndex], column.identifier);
|
|
});
|
|
}
|
|
|
|
contextMenu.appendItem(WI.UIString("Copy"), () => {
|
|
let rowIndexes;
|
|
if (table.isRowSelected(rowIndex))
|
|
rowIndexes = table.selectedRows;
|
|
else
|
|
rowIndexes = [rowIndex];
|
|
|
|
let cookies = this._cookiesAtIndexes(rowIndexes);
|
|
InspectorFrontendHost.copyText(this._formatCookiesAsText(cookies));
|
|
});
|
|
|
|
if (InspectorBackend.hasCommand("Page.deleteCookie")) {
|
|
contextMenu.appendItem(WI.UIString("Delete"), () => {
|
|
if (table.isRowSelected(rowIndex))
|
|
table.removeSelectedRows();
|
|
else
|
|
table.removeRow(rowIndex);
|
|
});
|
|
}
|
|
|
|
contextMenu.appendSeparator();
|
|
}
|
|
|
|
tableDidRemoveRows(table, rowIndexes)
|
|
{
|
|
if (!rowIndexes.length)
|
|
return;
|
|
|
|
for (let i = rowIndexes.length - 1; i >= 0; --i) {
|
|
let rowIndex = rowIndexes[i];
|
|
let cookie = this._filteredCookies[rowIndex];
|
|
console.assert(cookie, "Missing cookie for row " + rowIndex);
|
|
if (!cookie)
|
|
continue;
|
|
|
|
this._filteredCookies.splice(rowIndex, 1);
|
|
this._cookies.remove(cookie);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.PageAgent.deleteCookie(cookie.name, cookie.url);
|
|
}
|
|
}
|
|
|
|
tablePopulateCell(table, cell, column, rowIndex)
|
|
{
|
|
let cookie = this._filteredCookies[rowIndex];
|
|
|
|
cell.textContent = this._formatCookiePropertyForColumn(cookie, column);
|
|
|
|
if (!this._knownCells.has(cell)) {
|
|
this._knownCells.add(cell);
|
|
|
|
cell.addEventListener("dblclick", (event) => {
|
|
if (column.identifier === "size") {
|
|
InspectorFrontendHost.beep();
|
|
return;
|
|
}
|
|
|
|
this._showCookiePopover(cell, cookie, column.identifier);
|
|
});
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
|
|
// Popover delegate
|
|
|
|
willDismissPopover(popover)
|
|
{
|
|
if (popover instanceof WI.CookiePopover) {
|
|
this._willDismissCookiePopover(popover);
|
|
return;
|
|
}
|
|
|
|
console.assert();
|
|
}
|
|
|
|
// Protected
|
|
|
|
initialLayout()
|
|
{
|
|
super.initialLayout();
|
|
|
|
this._table = new WI.Table("cookies-table", this, this, 20);
|
|
this._table.allowsMultipleSelection = true;
|
|
|
|
this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
|
|
minWidth: 70,
|
|
maxWidth: 300,
|
|
initialWidth: 200,
|
|
resizeType: WI.TableColumn.ResizeType.Locked,
|
|
});
|
|
|
|
this._valueColumn = new WI.TableColumn("value", WI.UIString("Value"), {
|
|
minWidth: 100,
|
|
maxWidth: 600,
|
|
initialWidth: 200,
|
|
hideable: false,
|
|
});
|
|
|
|
this._domainColumn = new WI.TableColumn("domain", WI.unlocalizedString("Domain"), {
|
|
minWidth: 100,
|
|
maxWidth: 200,
|
|
initialWidth: 120,
|
|
});
|
|
|
|
this._pathColumn = new WI.TableColumn("path", WI.unlocalizedString("Path"), {
|
|
minWidth: 50,
|
|
maxWidth: 300,
|
|
initialWidth: 100,
|
|
});
|
|
|
|
this._expiresColumn = new WI.TableColumn("expires", WI.unlocalizedString("Expires"), {
|
|
minWidth: 100,
|
|
maxWidth: 200,
|
|
initialWidth: 150,
|
|
});
|
|
|
|
this._sizeColumn = new WI.TableColumn("size", WI.UIString("Size"), {
|
|
minWidth: 50,
|
|
maxWidth: 80,
|
|
initialWidth: 65,
|
|
align: "right",
|
|
});
|
|
|
|
this._secureColumn = new WI.TableColumn("secure", WI.unlocalizedString("Secure"), {
|
|
minWidth: 70,
|
|
maxWidth: 70,
|
|
align: "center",
|
|
});
|
|
|
|
this._httpOnlyColumn = new WI.TableColumn("httpOnly", WI.unlocalizedString("HttpOnly"), {
|
|
minWidth: 80,
|
|
maxWidth: 80,
|
|
align: "center",
|
|
});
|
|
|
|
this._sameSiteColumn = new WI.TableColumn("sameSite", WI.unlocalizedString("SameSite"), {
|
|
minWidth: 40,
|
|
maxWidth: 80,
|
|
initialWidth: 70,
|
|
align: "center",
|
|
});
|
|
|
|
this._table.addColumn(this._nameColumn);
|
|
this._table.addColumn(this._valueColumn);
|
|
this._table.addColumn(this._domainColumn);
|
|
this._table.addColumn(this._pathColumn);
|
|
this._table.addColumn(this._expiresColumn);
|
|
this._table.addColumn(this._sizeColumn);
|
|
this._table.addColumn(this._secureColumn);
|
|
this._table.addColumn(this._httpOnlyColumn);
|
|
this._table.addColumn(this._sameSiteColumn);
|
|
|
|
if (!this._table.sortColumnIdentifier) {
|
|
this._table.sortOrder = WI.Table.SortOrder.Ascending;
|
|
this._table.sortColumnIdentifier = "name";
|
|
}
|
|
|
|
this.addSubview(this._table);
|
|
|
|
this._table.element.addEventListener("keydown", this._handleTableKeyDown.bind(this));
|
|
|
|
this._reloadCookies();
|
|
}
|
|
|
|
// Private
|
|
|
|
_getCookiesForHost(cookies, host)
|
|
{
|
|
let resourceMatchesStorageDomain = (resource) => {
|
|
let urlComponents = resource.urlComponents;
|
|
return urlComponents && urlComponents.host && urlComponents.host === host;
|
|
};
|
|
|
|
let allResources = [];
|
|
for (let frame of WI.networkManager.frames) {
|
|
// The main resource isn't in the list of resources, so add it as a candidate.
|
|
allResources.push(frame.mainResource, ...frame.resourceCollection);
|
|
}
|
|
|
|
let resourcesForDomain = allResources.filter(resourceMatchesStorageDomain);
|
|
|
|
let cookiesForDomain = cookies.filter((cookie) => {
|
|
return resourcesForDomain.some((resource) => {
|
|
return WI.CookieStorageObject.cookieMatchesResourceURL(cookie, resource.url);
|
|
});
|
|
});
|
|
return cookiesForDomain;
|
|
}
|
|
|
|
_generateSortComparator()
|
|
{
|
|
let sortColumnIdentifier = this._table.sortColumnIdentifier;
|
|
if (!sortColumnIdentifier) {
|
|
this._sortComparator = null;
|
|
return;
|
|
}
|
|
|
|
let comparator = null;
|
|
|
|
switch (sortColumnIdentifier) {
|
|
case "name":
|
|
case "value":
|
|
case "domain":
|
|
case "path":
|
|
case "sameSite":
|
|
comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
|
|
break;
|
|
|
|
case "size":
|
|
case "httpOnly":
|
|
case "secure":
|
|
comparator = (a, b) => a[sortColumnIdentifier] - b[sortColumnIdentifier];
|
|
break;
|
|
|
|
case "expires":
|
|
comparator = (a, b) => {
|
|
if (!a.expires)
|
|
return 1;
|
|
if (!b.expires)
|
|
return -1;
|
|
return a.expires - b.expires;
|
|
};
|
|
break;
|
|
|
|
default:
|
|
console.assert("Unexpected sort column", sortColumnIdentifier);
|
|
return;
|
|
}
|
|
|
|
let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
|
|
this._sortComparator = (a, b) => reverseFactor * comparator(a, b);
|
|
}
|
|
|
|
_showCookiePopover(targetElement, cookie, columnIdentifier) {
|
|
console.assert(!this._editingCookie);
|
|
this._editingCookie = cookie;
|
|
|
|
let options = {};
|
|
if (columnIdentifier) {
|
|
switch (columnIdentifier) {
|
|
case "name":
|
|
case "value":
|
|
case "domain":
|
|
case "path":
|
|
case "secure":
|
|
options.focusField = columnIdentifier;
|
|
break;
|
|
|
|
case "expires":
|
|
options.focusField = this._editingCookie.session ? "session" : "expires";
|
|
break;
|
|
|
|
case "httpOnly":
|
|
options.focusField = "http-only";
|
|
break;
|
|
|
|
case "sameSite":
|
|
options.focusField = "same-site";
|
|
break;
|
|
|
|
default:
|
|
console.assert();
|
|
break;
|
|
}
|
|
}
|
|
|
|
let popover = new WI.CookiePopover(this);
|
|
popover.show(this._editingCookie, targetElement, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MIN_X, WI.RectEdge.MAX_X], options);
|
|
}
|
|
|
|
async _willDismissCookiePopover(popover)
|
|
{
|
|
let editingCookie = this._editingCookie;
|
|
this._editingCookie = null;
|
|
|
|
let serializedData = popover.serializedData;
|
|
if (!serializedData) {
|
|
InspectorFrontendHost.beep();
|
|
return;
|
|
}
|
|
|
|
let cookieToSet = WI.Cookie.fromPayload(serializedData);
|
|
|
|
let cookieProtocolPayload = cookieToSet.toProtocol();
|
|
if (!cookieProtocolPayload) {
|
|
InspectorFrontendHost.beep();
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
let promises = [];
|
|
if (editingCookie)
|
|
promises.push(target.PageAgent.deleteCookie(editingCookie.name, editingCookie.url));
|
|
promises.push(target.PageAgent.setCookie(cookieProtocolPayload));
|
|
promises.push(this._reloadCookies());
|
|
await Promise.all(promises);
|
|
|
|
let index = this._filteredCookies.findIndex((existingCookie) => cookieToSet.equals(existingCookie));
|
|
if (index >= 0)
|
|
this._table.selectRow(index);
|
|
}
|
|
|
|
_handleFilterBarFilterDidChange(event)
|
|
{
|
|
this._updateFilteredCookies();
|
|
this._updateEmptyFilterResultsMessage();
|
|
this._table.reloadData();
|
|
}
|
|
|
|
_handleSetCookieButtonClick(event)
|
|
{
|
|
this._showCookiePopover(this._setCookieButtonNavigationItem.element, null, "name");
|
|
}
|
|
|
|
_refreshButtonClicked(event)
|
|
{
|
|
this._reloadCookies();
|
|
}
|
|
|
|
_handleClearNavigationItemClicked(event)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
for (let cookie of this._cookies)
|
|
target.PageAgent.deleteCookie(cookie.name, cookie.url);
|
|
|
|
this._reloadCookies();
|
|
}
|
|
|
|
_reloadCookies()
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
if (!target.hasCommand("Page.getCookies"))
|
|
return;
|
|
|
|
target.PageAgent.getCookies().then((payload) => {
|
|
this._cookies = this._getCookiesForHost(payload.cookies.map(WI.Cookie.fromPayload), this.representedObject.host);
|
|
this._updateSort();
|
|
this._updateFilteredCookies();
|
|
this._updateEmptyFilterResultsMessage();
|
|
this._table.reloadData();
|
|
}).catch((error) => {
|
|
console.error("Could not fetch cookies: ", error);
|
|
});
|
|
}
|
|
|
|
_updateSort()
|
|
{
|
|
if (!this._sortComparator)
|
|
return;
|
|
|
|
this._cookies.sort(this._sortComparator);
|
|
}
|
|
|
|
_updateFilteredCookies()
|
|
{
|
|
this._filteredCookies = this._cookies;
|
|
|
|
let filterBar = this._filterBarNavigationItem.filterBar;
|
|
filterBar.invalid = false;
|
|
|
|
let filterText = filterBar.filters.text;
|
|
if (!filterText)
|
|
return;
|
|
|
|
let regex = WI.SearchUtilities.filterRegExpForString(filterText, WI.SearchUtilities.defaultSettings);
|
|
if (!regex) {
|
|
filterBar.invalid = true;
|
|
return;
|
|
}
|
|
|
|
this._filteredCookies = this._filteredCookies.filter((cookie) => {
|
|
for (let column of this._table.columns) {
|
|
let text = this._formatCookiePropertyForColumn(cookie, column);
|
|
if (text && regex.test(text))
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
_updateEmptyFilterResultsMessage()
|
|
{
|
|
if (this._filteredCookies.length || !this._filterBarNavigationItem.filterBar.filters.text) {
|
|
if (this._emptyFilterResultsMessageElement)
|
|
this._emptyFilterResultsMessageElement.remove();
|
|
this._emptyFilterResultsMessageElement = null;
|
|
} else {
|
|
if (!this._emptyFilterResultsMessageElement) {
|
|
let buttonElement = document.createElement("button");
|
|
buttonElement.textContent = WI.UIString("Clear Filters");
|
|
buttonElement.addEventListener("click", (event) => {
|
|
this._filterBarNavigationItem.filterBar.clear();
|
|
});
|
|
|
|
this._emptyFilterResultsMessageElement = WI.createMessageTextView(WI.UIString("No Filter Results"));
|
|
this._emptyFilterResultsMessageElement.appendChild(buttonElement);
|
|
}
|
|
|
|
this.element.appendChild(this._emptyFilterResultsMessageElement);
|
|
}
|
|
}
|
|
|
|
_handleTableKeyDown(event)
|
|
{
|
|
if (event.keyCode === WI.KeyboardShortcut.Key.Backspace.keyCode || event.keyCode === WI.KeyboardShortcut.Key.Delete.keyCode) {
|
|
if (InspectorBackend.hasCommand("Page.deleteCookie"))
|
|
this._table.removeSelectedRows();
|
|
else
|
|
InspectorFrontendHost.beep();
|
|
}
|
|
}
|
|
|
|
_cookiesAtIndexes(indexes)
|
|
{
|
|
if (!this._filteredCookies.length)
|
|
return [];
|
|
return indexes.map((index) => this._filteredCookies[index]);
|
|
}
|
|
|
|
_formatCookiesAsText(cookies)
|
|
{
|
|
let visibleColumns = this._table.columns.filter((column) => !column.hidden);
|
|
if (!visibleColumns.length)
|
|
return "";
|
|
|
|
let lines = cookies.map((cookie) => {
|
|
const usePunctuation = false;
|
|
let values = visibleColumns.map((column) => this._formatCookiePropertyForColumn(cookie, column, usePunctuation));
|
|
return values.join("\t");
|
|
});
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
_formatCookiePropertyForColumn(cookie, column, usePunctuation = true)
|
|
{
|
|
const checkmark = "\u2713";
|
|
const missingValue = usePunctuation ? emDash : "";
|
|
const missingCheckmark = usePunctuation ? zeroWidthSpace : "";
|
|
|
|
switch (column.identifier) {
|
|
case "name":
|
|
return cookie.name;
|
|
case "value":
|
|
return cookie.value;
|
|
case "domain":
|
|
return cookie.domain || missingValue;
|
|
case "path":
|
|
return cookie.path || missingValue;
|
|
case "expires":
|
|
return (!cookie.session && cookie.expires) ? cookie.expires.toLocaleString() : WI.UIString("Session");
|
|
case "size":
|
|
return Number.bytesToString(cookie.size);
|
|
case "secure":
|
|
return cookie.secure ? checkmark : missingCheckmark;
|
|
case "httpOnly":
|
|
return cookie.httpOnly ? checkmark : missingCheckmark;
|
|
case "sameSite":
|
|
return cookie.sameSite === WI.Cookie.SameSiteType.None ? missingValue : WI.Cookie.displayNameForSameSiteType(cookie.sameSite);
|
|
}
|
|
|
|
console.assert("Unexpected table column " + column.identifier);
|
|
return "";
|
|
}
|
|
};
|
|
|
|
WI.CookieType = {
|
|
Request: 0,
|
|
Response: 1
|
|
};
|