// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/*
* Copyright (C) 2009 Apple Inc. All rights reserved.
* Copyright (C) 2009 Joseph Pecoraro
*
* 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.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE 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 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.
*/
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Geometry from '../../models/geometry/geometry.js';
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
import * as CookieTable from '../../ui/legacy/components/cookie_table/cookie_table.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import cookieItemsViewStyles from './cookieItemsView.css.js';
import {StorageItemsToolbar} from './StorageItemsToolbar.js';
const UIStrings = {
/**
* @description Label for checkbox to show URL-decoded cookie values
*/
showUrlDecoded: 'Show URL-decoded',
/**
* @description Text in Cookie Items View of the Application panel to indicate that no cookie has been selected for preview
*/
noCookieSelected: 'No cookie selected',
/**
* @description Text in Cookie Items View of the Application panel
*/
selectACookieToPreviewItsValue: 'Select a cookie to preview its value',
/**
* @description Text for filter in Cookies View of the Application panel
*/
onlyShowCookiesWithAnIssue: 'Only show cookies with an issue',
/**
* @description Title for filter in the Cookies View of the Application panel
*/
onlyShowCookiesWhichHaveAn: 'Only show cookies that have an associated issue',
/**
* @description Label to only delete the cookies that are visible after filtering
*/
clearFilteredCookies: 'Clear filtered cookies',
/**
* @description Label to delete all cookies
*/
clearAllCookies: 'Clear all cookies',
/**
* @description Alert message for screen reader to announce # of cookies in the table
* @example {5} PH1
*/
numberOfCookiesShownInTableS: 'Number of cookies shown in table: {PH1}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/CookieItemsView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {Size} = Geometry;
interface CookiePreviewWidgetInput {
cookie: SDK.Cookie.Cookie|null;
showDecoded: boolean;
onShowDecodedChanged: (showDecoded: boolean) => void;
}
type CookiePreviewWidgetView = (input: CookiePreviewWidgetInput, output: undefined, target: HTMLElement) => void;
export const DEFAULT_COOKIE_PREVIEW_WIDGET_VIEW: CookiePreviewWidgetView = (input, output, target) => {
const cookieValue =
input.cookie ? (input.showDecoded ? decodeURIComponent(input.cookie.value()) : input.cookie.value()) : '';
function handleDblClickOnCookieValue(event: Event): void {
event.preventDefault();
const range = document.createRange();
range.selectNode(event.currentTarget as Node);
const selection = window.getSelection();
if (!selection) {
return;
}
selection.removeAllRanges();
selection.addRange(range);
}
// clang-format off
render(html`
`,
// clang-format on
target);
};
class CookiePreviewWidget extends UI.Widget.VBox {
private view: CookiePreviewWidgetView;
#cookie: SDK.Cookie.Cookie|null;
private showDecodedSetting: Common.Settings.Setting;
constructor(element?: HTMLElement, view: CookiePreviewWidgetView = DEFAULT_COOKIE_PREVIEW_WIDGET_VIEW) {
super(element, {jslog: `${VisualLogging.section('cookie-preview')}`});
this.view = view;
this.setMinimumSize(230, 45);
this.#cookie = null;
this.showDecodedSetting = Common.Settings.Settings.instance().createSetting('cookie-view-show-decoded', false);
this.requestUpdate();
}
set cookie(cookie: SDK.Cookie.Cookie|null) {
this.#cookie = cookie;
this.requestUpdate();
}
override performUpdate(): void {
const input: CookiePreviewWidgetInput = {
cookie: this.#cookie,
showDecoded: this.showDecodedSetting.get(),
onShowDecodedChanged: (showDecoded: boolean) => {
this.showDecodedSetting.set(showDecoded);
this.requestUpdate();
},
};
this.view(input, undefined, this.contentElement);
}
}
interface CookieItemsViewInput {
cookieDomain: string;
cookiesData: CookieTable.CookiesTable.CookiesTableData;
onSaveCookie: (arg0: SDK.Cookie.Cookie, arg1: SDK.Cookie.Cookie|null) => Promise;
onRefresh: () => void;
onSelect: (arg0: SDK.Cookie.Cookie|null) => void;
onDelete: (arg0: SDK.Cookie.Cookie, arg1: () => void) => void;
onDeleteSelectedItems: () => void;
onDeleteAllItems: () => void;
onRefreshItems: () => void;
selectedCookie: SDK.Cookie.Cookie|null;
}
interface CookieItemsViewOutput {
toolbar: StorageItemsToolbar;
}
type View = (input: CookieItemsViewInput, output: CookieItemsViewOutput, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
// clang-format off
render(html`
{ output.toolbar = toolbar; })}
>
${input.selectedCookie ?
html`` :
html``}
`,
// clang-format on
target);
};
export class CookieItemsView extends UI.Widget.VBox {
private view: View;
private model: SDK.CookieModel.CookieModel;
private cookieDomain: string;
private onlyIssuesFilterUI: UI.Toolbar.ToolbarCheckbox;
private allCookies: SDK.Cookie.Cookie[];
private shownCookies: SDK.Cookie.Cookie[];
private selectedCookie: SDK.Cookie.Cookie|null;
#toolbar?: StorageItemsToolbar;
constructor(model: SDK.CookieModel.CookieModel, cookieDomain: string, view: View = DEFAULT_VIEW) {
super({jslog: `${VisualLogging.pane('cookies-data')}`});
this.view = view;
this.model = model;
this.cookieDomain = cookieDomain;
this.onlyIssuesFilterUI = new UI.Toolbar.ToolbarCheckbox(
i18nString(UIStrings.onlyShowCookiesWithAnIssue), i18nString(UIStrings.onlyShowCookiesWhichHaveAn), () => {
this.updateWithCookies(this.allCookies);
}, 'only-show-cookies-with-issues');
this.allCookies = [];
this.shownCookies = [];
this.selectedCookie = null;
this.setCookiesDomain(model, cookieDomain);
this.requestUpdate();
}
setCookiesDomain(model: SDK.CookieModel.CookieModel, domain: string): void {
this.model.removeEventListener(SDK.CookieModel.Events.COOKIE_LIST_UPDATED, this.onCookieListUpdate, this);
this.model = model;
this.cookieDomain = domain;
this.refreshItems();
this.model.addEventListener(SDK.CookieModel.Events.COOKIE_LIST_UPDATED, this.onCookieListUpdate, this);
}
override performUpdate(): void {
const that = this;
const output = {
set toolbar(toolbar: StorageItemsToolbar) {
if (that.#toolbar === toolbar) {
return;
}
that.#toolbar = toolbar;
that.#toolbar.appendToolbarItem(that.onlyIssuesFilterUI);
that.updateWithCookies(that.allCookies);
},
};
const cookiesData: CookieTable.CookiesTable.CookiesTableData = {
cookies: this.shownCookies,
cookieToBlockedReasons: this.model.getCookieToBlockedReasonsMap(),
};
const parsedURL = Common.ParsedURL.ParsedURL.fromString(this.cookieDomain);
const host = parsedURL ? parsedURL.host : '';
const input: CookieItemsViewInput = {
cookieDomain: host,
cookiesData,
onSaveCookie: this.saveCookie.bind(this),
onRefresh: this.refreshItems.bind(this),
onSelect: this.handleCookieSelected.bind(this),
onDelete: this.deleteCookie.bind(this),
onDeleteSelectedItems: this.deleteSelectedItem.bind(this),
onDeleteAllItems: this.deleteAllItems.bind(this),
onRefreshItems: this.refreshItems.bind(this),
selectedCookie: this.selectedCookie,
};
this.view(input, output, this.contentElement);
}
override wasShown(): void {
super.wasShown();
this.refreshItems();
}
private showPreview(cookie: SDK.Cookie.Cookie|null): void {
if (cookie === this.selectedCookie) {
return;
}
this.selectedCookie = cookie;
this.requestUpdate();
}
private handleCookieSelected(selectedCookie: SDK.Cookie.Cookie|null): void {
if (!this.#toolbar) {
return;
}
this.#toolbar.setCanDeleteSelected(Boolean(selectedCookie));
this.showPreview(selectedCookie);
}
private async saveCookie(newCookie: SDK.Cookie.Cookie, oldCookie: SDK.Cookie.Cookie|null): Promise {
if (oldCookie && newCookie.key() !== oldCookie.key()) {
await this.model.deleteCookie(oldCookie);
}
return await this.model.saveCookie(newCookie);
}
private deleteCookie(cookie: SDK.Cookie.Cookie, callback: () => void): void {
void this.model.deleteCookie(cookie).then(callback);
}
private updateWithCookies(allCookies: SDK.Cookie.Cookie[]): void {
if (!this.#toolbar) {
return;
}
this.allCookies = allCookies;
this.shownCookies = this.filter(allCookies, cookie => `${cookie.name()} ${cookie.value()} ${cookie.domain()}`);
if (this.#toolbar.hasFilter()) {
this.#toolbar.setDeleteAllTitle(i18nString(UIStrings.clearFilteredCookies));
this.#toolbar.setDeleteAllGlyph('filter-clear');
} else {
this.#toolbar.setDeleteAllTitle(i18nString(UIStrings.clearAllCookies));
this.#toolbar.setDeleteAllGlyph('clear-list');
}
UI.ARIAUtils.LiveAnnouncer.alert(
i18nString(UIStrings.numberOfCookiesShownInTableS, {PH1: this.shownCookies.length}));
this.#toolbar.setCanFilter(true);
this.#toolbar.setCanDeleteAll(this.shownCookies.length > 0);
this.#toolbar.setCanDeleteSelected(Boolean(this.selectedCookie));
this.requestUpdate();
}
filter(items: T[], keyFunction: (arg0: T) => string): T[] {
const predicate = (object: T|null): boolean => {
if (!this.onlyIssuesFilterUI.checked()) {
return true;
}
if (object instanceof SDK.Cookie.Cookie) {
return IssuesManager.RelatedIssue.hasIssues(object);
}
return false;
};
return items.filter(item => this.#toolbar?.filterRegex?.test(keyFunction(item)) ?? true).filter(predicate);
}
/**
* This will only delete the currently visible cookies.
*/
deleteAllItems(): void {
this.showPreview(null);
void this.model.deleteCookies(this.shownCookies);
}
deleteSelectedItem(): void {
if (this.selectedCookie) {
this.showPreview(null);
void this.model.deleteCookie(this.selectedCookie);
}
}
private onCookieListUpdate(): void {
void this.model.getCookiesForDomain(this.cookieDomain).then(this.updateWithCookies.bind(this));
}
refreshItems(): void {
void this.model.getCookiesForDomain(this.cookieDomain, true).then(this.updateWithCookies.bind(this));
}
}