chrome-devtools-frontend
Version:
Chrome DevTools UI
240 lines (214 loc) • 8.47 kB
text/typescript
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Protocol from '../../generated/protocol.js';
import * as Common from '../common/common.js';
import * as Platform from '../platform/platform.js';
import * as Root from '../root/root.js';
import {type Attribute, Cookie} from './Cookie.js';
import {Events as NetworkManagerEvents, NetworkManager} from './NetworkManager.js';
import type {Resource} from './Resource.js';
import {Events as ResourceTreeModelEvents, ResourceTreeModel} from './ResourceTreeModel.js';
import {SDKModel} from './SDKModel.js';
import {Capability, type Target} from './Target.js';
export class CookieModel extends SDKModel<EventTypes> {
readonly #blockedCookies = new Map<string, Cookie>();
readonly #cookieToBlockedReasons = new Map<Cookie, BlockedReason[]>();
readonly #refreshThrottler = new Common.Throttler.Throttler(300);
#cookies = new Map<string, Cookie[]>();
constructor(target: Target) {
super(target);
target.model(ResourceTreeModel)
?.addEventListener(ResourceTreeModelEvents.PrimaryPageChanged, this.#onPrimaryPageChanged, this);
target.model(NetworkManager)
?.addEventListener(NetworkManagerEvents.ResponseReceived, this.#onResponseReceived, this);
target.model(NetworkManager)?.addEventListener(NetworkManagerEvents.LoadingFinished, this.#onLoadingFinished, this);
}
addBlockedCookie(cookie: Cookie, blockedReasons: BlockedReason[]|null): void {
const key = cookie.key();
const previousCookie = this.#blockedCookies.get(key);
this.#blockedCookies.set(key, cookie);
if (blockedReasons) {
this.#cookieToBlockedReasons.set(cookie, blockedReasons);
} else {
this.#cookieToBlockedReasons.delete(cookie);
}
if (previousCookie) {
this.#cookieToBlockedReasons.delete(previousCookie);
}
}
removeBlockedCookie(cookie: Cookie): void {
this.#blockedCookies.delete(cookie.key());
}
async #onPrimaryPageChanged(): Promise<void> {
this.#blockedCookies.clear();
this.#cookieToBlockedReasons.clear();
await this.#refresh();
}
getCookieToBlockedReasonsMap(): ReadonlyMap<Cookie, BlockedReason[]> {
return this.#cookieToBlockedReasons;
}
async #getCookies(urls: Platform.MapUtilities.Multimap<string, string>): Promise<void> {
const networkAgent = this.target().networkAgent();
const newCookies = new Map<string, Cookie[]>(await Promise.all(urls.keysArray().map(
domain => networkAgent.invoke_getCookies({urls: [...urls.get(domain).values()]})
.then(({cookies}) => [domain, cookies.map(Cookie.fromProtocolCookie)] as const))));
const updated = this.#isUpdated(newCookies);
this.#cookies = newCookies;
if (updated) {
this.dispatchEventToListeners(Events.COOKIE_LIST_UPDATED);
}
}
async deleteCookie(cookie: Cookie): Promise<void> {
await this.deleteCookies([cookie]);
}
async clear(domain?: string, securityOrigin?: string): Promise<void> {
if (!this.#isRefreshing()) {
await this.#refreshThrottled();
}
const cookies = domain ? (this.#cookies.get(domain) || []) : [...this.#cookies.values()].flat();
cookies.push(...this.#blockedCookies.values());
if (securityOrigin) {
const cookiesToDelete = cookies.filter(cookie => {
return cookie.matchesSecurityOrigin(securityOrigin);
});
await this.deleteCookies(cookiesToDelete);
} else {
await this.deleteCookies(cookies);
}
}
async saveCookie(cookie: Cookie): Promise<boolean> {
let domain = cookie.domain();
if (!domain.startsWith('.')) {
domain = '';
}
let expires: number|undefined = undefined;
if (cookie.expires()) {
expires = Math.floor(Date.parse(`${cookie.expires()}`) / 1000);
}
const enabled = Root.Runtime.experiments.isEnabled('experimental-cookie-features');
const preserveUnset = (scheme: Protocol.Network.CookieSourceScheme): Protocol.Network.CookieSourceScheme.Unset|
undefined => scheme === Protocol.Network.CookieSourceScheme.Unset ? scheme : undefined;
const protocolCookie = {
name: cookie.name(),
value: cookie.value(),
url: cookie.url() || undefined,
domain,
path: cookie.path(),
secure: cookie.secure(),
httpOnly: cookie.httpOnly(),
sameSite: cookie.sameSite(),
expires,
priority: cookie.priority(),
partitionKey: cookie.partitionKey(),
sourceScheme: enabled ? cookie.sourceScheme() : preserveUnset(cookie.sourceScheme()),
sourcePort: enabled ? cookie.sourcePort() : undefined,
};
const response = await this.target().networkAgent().invoke_setCookie(protocolCookie);
const error = response.getError();
if (error || !response.success) {
return false;
}
await this.#refreshThrottled();
return response.success;
}
/**
* Returns cookies needed by current page's frames whose security origins are |domain|.
*/
async getCookiesForDomain(domain: string, forceUpdate?: boolean): Promise<Cookie[]> {
if (!this.#isRefreshing() || forceUpdate) {
await this.#refreshThrottled();
}
const normalCookies = this.#cookies.get(domain) || [];
return normalCookies.concat(Array.from(this.#blockedCookies.values()));
}
async deleteCookies(cookies: Cookie[]): Promise<void> {
const networkAgent = this.target().networkAgent();
this.#blockedCookies.clear();
this.#cookieToBlockedReasons.clear();
await Promise.all(cookies.map(cookie => networkAgent.invoke_deleteCookies({
name: cookie.name(),
url: undefined,
domain: cookie.domain(),
path: cookie.path(),
partitionKey: cookie.partitionKey(),
})));
await this.#refreshThrottled();
}
#isRefreshing(): boolean {
return Boolean(this.listeners?.size);
}
#isUpdated(newCookies: Map<string, Cookie[]>): boolean {
if (newCookies.size !== this.#cookies.size) {
return true;
}
for (const [domain, newDomainCookies] of newCookies) {
if (!this.#cookies.has(domain)) {
return true;
}
const oldDomainCookies = this.#cookies.get(domain) || [];
if (newDomainCookies.length !== oldDomainCookies.length) {
return true;
}
const comparisonKey = (c: Cookie): string => c.key() + ' ' + c.value();
const oldDomainCookieKeys = new Set(oldDomainCookies.map(comparisonKey));
for (const newCookie of newDomainCookies) {
if (!oldDomainCookieKeys.has(comparisonKey(newCookie))) {
return true;
}
}
}
return false;
}
#refreshThrottled(): Promise<void> {
return this.#refreshThrottler.schedule(() => this.#refresh());
}
#refresh(): Promise<void> {
const resourceURLs = new Platform.MapUtilities.Multimap<string, string>();
function populateResourceURLs(resource: Resource): boolean {
const documentURL = Common.ParsedURL.ParsedURL.fromString(resource.documentURL);
if (documentURL) {
resourceURLs.set(documentURL.securityOrigin(), resource.url);
}
return false;
}
const resourceTreeModel = this.target().model(ResourceTreeModel);
if (resourceTreeModel) {
// In case the current frame was unreachable, add its cookies
// because they might help to debug why the frame was unreachable.
const unreachableUrl = resourceTreeModel.mainFrame?.unreachableUrl();
if (unreachableUrl) {
const documentURL = Common.ParsedURL.ParsedURL.fromString(unreachableUrl);
if (documentURL) {
resourceURLs.set(documentURL.securityOrigin(), unreachableUrl);
}
}
resourceTreeModel.forAllResources(populateResourceURLs);
}
return this.#getCookies(resourceURLs);
}
#onResponseReceived(): void {
if (this.#isRefreshing()) {
void this.#refreshThrottled();
}
}
#onLoadingFinished(): void {
if (this.#isRefreshing()) {
void this.#refreshThrottled();
}
}
}
SDKModel.register(CookieModel, {capabilities: Capability.NETWORK, autostart: false});
export interface BlockedReason {
uiString: string;
attribute: Attribute|null;
}
export interface ExemptionReason {
uiString: string;
}
export const enum Events {
COOKIE_LIST_UPDATED = 'CookieListUpdated',
}
export interface EventTypes {
[Events.COOKIE_LIST_UPDATED]: void;
}