/* * 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 };