happy-dom
Version:
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.
227 lines (204 loc) • 6.6 kB
text/typescript
import EventTarget from '../event/EventTarget.js';
import type BrowserWindow from '../window/BrowserWindow.js';
import type ICookieStoreGetOptions from './ICookieStoreGetOptions.js';
import type ICookieStoreSetOptions from './ICookieStoreSetOptions.js';
import type ICookieStoreDeleteOptions from './ICookieStoreDeleteOptions.js';
import type ICookieStoreItem from './ICookieStoreItem.js';
import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js';
import CookieSameSiteEnum from '../cookie/enums/CookieSameSiteEnum.js';
import CookieChangeEvent from '../event/events/CookieChangeEvent.js';
import type ICookie from '../cookie/ICookie.js';
import URL from '../url/URL.js';
import WindowBrowserContext from '../window/WindowBrowserContext.js';
import * as PropertySymbol from '../PropertySymbol.js';
/**
* CookieStore.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/CookieStore
*/
export default class CookieStore extends EventTarget {
// Internal properties
readonly #window: BrowserWindow;
/**
* Constructor.
*
* @param illegalConstructorSymbol Illegal constructor symbol.
* @param window Window.
*/
constructor(illegalConstructorSymbol: symbol, window: BrowserWindow) {
super();
if (illegalConstructorSymbol !== PropertySymbol.illegalConstructor) {
throw new TypeError('Illegal constructor');
}
this.#window = window;
}
/**
* Returns a cookie.
*
* @param [nameOrOptions] Name or options.
* @returns Cookie.
*/
public async get(
nameOrOptions?: string | ICookieStoreGetOptions
): Promise<ICookieStoreItem | null> {
const cookies = await this.getAll(nameOrOptions);
return cookies.length > 0 ? cookies[0] : null;
}
/**
* Returns all cookies.
*
* @param [nameOrOptions] Name or options.
* @returns Cookies.
*/
public async getAll(
nameOrOptions?: string | ICookieStoreGetOptions
): Promise<ICookieStoreItem[]> {
const browserFrame = new WindowBrowserContext(this.#window).getBrowserFrame();
if (!browserFrame) {
return [];
}
const options: ICookieStoreGetOptions =
typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions || {};
let targetURL: URL;
if (options.url) {
targetURL = new URL(options.url, this.#window.location.href);
// In a document context, only the current URL is valid
if (targetURL.origin !== this.#window.location.origin) {
throw new this.#window.DOMException(
`Failed to execute 'getAll' on 'CookieStore': URL must match the document origin`,
DOMExceptionNameEnum.securityError
);
}
} else {
targetURL = <URL>(<unknown>this.#window.location);
}
const internalCookies = browserFrame.page.context.cookieContainer.getCookies(targetURL, true);
const result: ICookieStoreItem[] = [];
for (const cookie of internalCookies) {
if (!options.name || cookie.key === options.name) {
result.push(this.#convertToStoreItem(cookie));
}
}
return result;
}
/**
* Sets a cookie.
*
* @param nameOrOptions Name or options.
* @param [value] Value.
* @returns Promise.
*/
public async set(nameOrOptions: string | ICookieStoreSetOptions, value?: string): Promise<void> {
const browserFrame = new WindowBrowserContext(this.#window).getBrowserFrame();
if (!browserFrame) {
return;
}
let options: ICookieStoreSetOptions;
if (typeof nameOrOptions === 'string') {
if (value === undefined) {
throw new this.#window.TypeError(
`Failed to execute 'set' on 'CookieStore': Value is required when name is provided as a string.`
);
}
options = { name: nameOrOptions, value };
} else {
options = { ...nameOrOptions, value: nameOrOptions.value ?? '' };
}
if (!options.name) {
throw new this.#window.TypeError(
`Failed to execute 'set' on 'CookieStore': Required member name is undefined.`
);
}
const originURL = <URL>(<unknown>this.#window.location);
// Convert expires to Date if it's a number (Unix timestamp in milliseconds)
let expires: Date | null = null;
if (options.expires !== undefined && options.expires !== null) {
expires = options.expires instanceof Date ? options.expires : new Date(options.expires);
}
// Convert sameSite string to enum
let sameSite = CookieSameSiteEnum.strict;
if (options.sameSite) {
switch (options.sameSite.toLowerCase()) {
case 'strict':
sameSite = CookieSameSiteEnum.strict;
break;
case 'lax':
sameSite = CookieSameSiteEnum.lax;
break;
case 'none':
sameSite = CookieSameSiteEnum.none;
break;
}
}
const cookie: ICookie = {
key: options.name,
value: options.value ?? '',
originURL,
domain: options.domain || '',
path: options.path || '/',
expires,
httpOnly: false, // CookieStore API cannot set httpOnly cookies
secure: true,
sameSite
};
const cookieContainer = browserFrame.page.context.cookieContainer;
const changedCookies = cookieContainer.addCookies([cookie]);
if (changedCookies.changed.length > 0 || changedCookies.deleted.length > 0) {
this.dispatchEvent(
new CookieChangeEvent('change', {
changed: changedCookies.changed.map((cookie) => this.#convertToStoreItem(cookie)),
deleted: changedCookies.deleted.map((cookie) => this.#convertToStoreItem(cookie))
})
);
}
}
/**
* Deletes a cookie.
*
* @param nameOrOptions Name or options.
* @returns Promise.
*/
public async delete(nameOrOptions: string | ICookieStoreDeleteOptions): Promise<void> {
const options: ICookieStoreDeleteOptions =
typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions;
// Set the cookie with an expired date to delete it
await this.set({
name: options.name,
value: '',
domain: options.domain ?? undefined,
path: options.path,
partitioned: options.partitioned,
expires: new Date(0)
});
}
/**
* Converts an internal cookie to a CookieStore item.
*
* @param cookie Internal cookie.
* @returns CookieStore item.
*/
#convertToStoreItem(cookie: ICookie): ICookieStoreItem {
let sameSite: 'strict' | 'lax' | 'none' = 'lax';
switch (cookie.sameSite) {
case CookieSameSiteEnum.strict:
sameSite = 'strict';
break;
case CookieSameSiteEnum.lax:
sameSite = 'lax';
break;
case CookieSameSiteEnum.none:
sameSite = 'none';
break;
}
return {
name: cookie.key,
value: cookie.value ?? '',
domain: cookie.domain,
path: cookie.path,
expires: cookie.expires ? cookie.expires.getTime() : null,
secure: cookie.secure,
sameSite,
partitioned: false // Not supported yet
};
}
}