From 643e19905bc8f800c18e023cea55ba8e20413d55 Mon Sep 17 00:00:00 2001 From: Mas0nShi Date: Wed, 30 Nov 2022 15:12:51 +0800 Subject: [PATCH] #666@patch: Syntax fix. --- packages/happy-dom/src/cookie/Cookie.ts | 252 ++++++++++++--------- packages/happy-dom/src/cookie/CookieJar.ts | 133 ++++++----- 2 files changed, 221 insertions(+), 164 deletions(-) diff --git a/packages/happy-dom/src/cookie/Cookie.ts b/packages/happy-dom/src/cookie/Cookie.ts index ddcf02813..682856aa1 100644 --- a/packages/happy-dom/src/cookie/Cookie.ts +++ b/packages/happy-dom/src/cookie/Cookie.ts @@ -1,122 +1,158 @@ const CookiePairRegex = /([^=]+)(?:=([\s\S]*))?/; - +/** + * Cookie. + */ export default class Cookie { - private pairs: { [key: string]: string } = {}; - // - public key: string = ''; - public value: string = ''; - public size: number = 0; - // Optional - public domain: string = ''; - public path: string = ''; - public expriesOrMaxAge: Date = null; - public httpOnly: boolean = false; - public secure: boolean = false; - public sameSite: string = ''; - + private pairs: { [key: string]: string } = {}; + // + public key = ''; + public value = ''; + public size = 0; + // Optional + public domain = ''; + public path = ''; + public expriesOrMaxAge: Date = null; + public httpOnly = false; + public secure = false; + public sameSite = ''; - constructor(cookie: string) { - let match: RegExpExecArray | null; + /** + * Constructor. + * + * @param cookie Cookie. + */ + constructor(cookie: string) { + let match: RegExpExecArray | null; - const parts = cookie.split(";").filter(Boolean); + const parts = cookie.split(';').filter(Boolean); - // Part[0] is the key-value pair. - match = new RegExp(CookiePairRegex).exec(parts[0]); - if (!match) { - throw new Error(`Invalid cookie: ${cookie}`); - } - this.key = match[1].trim(); - this.value = match[2]; - // set key is empty if match[2] is undefined. - if (!match[2]) { - this.value = this.key; - this.key = ''; - } - this.pairs[this.key] = this.value; - this.size = this.key.length + this.value.length; - // Attribute. - for (const part of parts.slice(1)) { - match = new RegExp(CookiePairRegex).exec(part); - if (!match) { - throw new Error(`Invalid cookie: ${part}`); - } - const key = match[1].trim(); - const value = match[2]; - - switch (key.toLowerCase()) { - case "expires": - this.expriesOrMaxAge = new Date(value); - break; - case "max-age": - this.expriesOrMaxAge = new Date(parseInt(value, 10) * 1000 + Date.now()); - break; - case "domain": - this.domain = value; - break; - case "path": - this.path = value.startsWith("/") ? value : `/${value}`; - break; - case "httponly": - this.httpOnly = true; - break; - case "secure": - this.secure = true; - break; - case "samesite": - this.sameSite = value; - break; - default: - continue; // skip. - } - // skip unknown key-value pair. - if (['expires', 'max-age', 'domain', 'path', 'httponly', 'secure', 'samesite'].indexOf(key.toLowerCase()) === -1) { - continue; - } - this.pairs[key] = value; - } - } + // Part[0] is the key-value pair. + match = new RegExp(CookiePairRegex).exec(parts[0]); + if (!match) { + throw new Error(`Invalid cookie: ${cookie}`); + } + this.key = match[1].trim(); + this.value = match[2]; + // Set key is empty if match[2] is undefined. + if (!match[2]) { + this.value = this.key; + this.key = ''; + } + this.pairs[this.key] = this.value; + this.size = this.key.length + this.value.length; + // Attribute. + for (const part of parts.slice(1)) { + match = new RegExp(CookiePairRegex).exec(part); + if (!match) { + throw new Error(`Invalid cookie: ${part}`); + } + const key = match[1].trim(); + const value = match[2]; - public rawString(): string { - return Object.keys(this.pairs).map(key => { - if (key) { - return `${key}=${this.pairs[key]}`; - } - return this.pairs[key]; - }).join("; "); - } + switch (key.toLowerCase()) { + case 'expires': + this.expriesOrMaxAge = new Date(value); + break; + case 'max-age': + this.expriesOrMaxAge = new Date(parseInt(value, 10) * 1000 + Date.now()); + break; + case 'domain': + this.domain = value; + break; + case 'path': + this.path = value.startsWith('/') ? value : `/${value}`; + break; + case 'httponly': + this.httpOnly = true; + break; + case 'secure': + this.secure = true; + break; + case 'samesite': + this.sameSite = value; + break; + default: + continue; // Skip. + } + // Skip unknown key-value pair. + if ( + ['expires', 'max-age', 'domain', 'path', 'httponly', 'secure', 'samesite'].indexOf( + key.toLowerCase() + ) === -1 + ) { + continue; + } + this.pairs[key] = value; + } + } - public cookieString(): string { - if (this.key) { - return `${this.key}=${this.value}`; - } - return this.value; - } + /** + * Returns a raw string of the cookie. + */ + public rawString(): string { + return Object.keys(this.pairs) + .map((key) => { + if (key) { + return `${key}=${this.pairs[key]}`; + } + return this.pairs[key]; + }) + .join('; '); + } + /** + * + */ + public cookieString(): string { + if (this.key) { + return `${this.key}=${this.value}`; + } + return this.value; + } - public isExpired(): boolean { - // If the expries/maxage is set, then determine whether it is expired. - if (this.expriesOrMaxAge && this.expriesOrMaxAge.getTime() < Date.now()) { - return true; - } - // If the expries/maxage is not set, it's a session-level cookie that will expire when the browser is closed. - // (it's never expired in happy-dom) - return false; - } + /** + * + */ + public isExpired(): boolean { + // If the expries/maxage is set, then determine whether it is expired. + if (this.expriesOrMaxAge && this.expriesOrMaxAge.getTime() < Date.now()) { + return true; + } + // If the expries/maxage is not set, it's a session-level cookie that will expire when the browser is closed. + // (it's never expired in happy-dom) + return false; + } - public isHttpOnly(): boolean { - return this.httpOnly; - } + /** + * + */ + public isHttpOnly(): boolean { + return this.httpOnly; + } - public isSecure(): boolean { - return this.secure; - } + /** + * + */ + public isSecure(): boolean { + return this.secure; + } - public static parse(cookieString: string): Cookie { - return new Cookie(cookieString); - } + /** + * Parse a cookie string. + * + * @param cookieString + */ + public static parse(cookieString: string): Cookie { + return new Cookie(cookieString); + } - public static stringify(cookie: Cookie): string { - return cookie.toString(); - } -} \ No newline at end of file + /** + * Stringify a Cookie object. + * + * @param cookie + */ + public static stringify(cookie: Cookie): string { + return cookie.toString(); + } +} diff --git a/packages/happy-dom/src/cookie/CookieJar.ts b/packages/happy-dom/src/cookie/CookieJar.ts index ce3a9930f..8cd6a2e3c 100644 --- a/packages/happy-dom/src/cookie/CookieJar.ts +++ b/packages/happy-dom/src/cookie/CookieJar.ts @@ -1,61 +1,82 @@ -import Location from "src/location/Location"; -import Cookie from "./Cookie"; - +import Location from 'src/location/Location'; +import Cookie from './Cookie'; +/** + * CookieJar. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie. + */ export default class CookieJar { - private cookies: Cookie[] = []; - + private cookies: Cookie[] = []; - private validateCookie(cookie: Cookie): boolean { - if (cookie.key.toLocaleUpperCase().startsWith('__secure-') && !cookie.isSecure()) return false; - if (cookie.key.toLocaleUpperCase().startsWith('__host-') && (!cookie.isSecure() || cookie.path !== '/' || cookie.domain)) return false; - return true; - } + /** + * Validate cookie. + * + * @param cookie + */ + private validateCookie(cookie: Cookie): boolean { + if (cookie.key.toLocaleUpperCase().startsWith('__secure-') && !cookie.isSecure()) { + return false; + } + if ( + cookie.key.toLocaleUpperCase().startsWith('__host-') && + (!cookie.isSecure() || cookie.path !== '/' || cookie.domain) + ) { + return false; + } + return true; + } - public setCookiesString(cookieString: string): void { - if (!cookieString) return; - const newCookie = new Cookie(cookieString); - if (!this.validateCookie(newCookie)) { - return; - } - this.cookies.filter(cookie => cookie.key === newCookie.key).forEach(cookie => { - this.cookies.splice(this.cookies.indexOf(cookie), 1); - }); - this.cookies.push(newCookie); - } + /** + * Set cookie. + * + * @param cookieString + */ + public setCookiesString(cookieString: string): void { + if (!cookieString) { + return; + } + const newCookie = new Cookie(cookieString); + if (!this.validateCookie(newCookie)) { + return; + } + this.cookies + .filter((cookie) => cookie.key === newCookie.key) + .forEach((cookie) => { + this.cookies.splice(this.cookies.indexOf(cookie), 1); + }); + this.cookies.push(newCookie); + } - // return the cookie string. - // skip httponly when use document.cookie. - public getCookiesString(url: Location, fromDocument: boolean): string { - const cookies = this.cookies.filter(cookie => { - // skip when use document.cookie and the cookie is httponly. - if (fromDocument && cookie.isHttpOnly()) return false; - if (cookie.isExpired()) return false; - if (cookie.isSecure() && url.protocol !== "https:") return false; - if (cookie.domain && !url.hostname.endsWith(cookie.domain)) return false; - if (cookie.path && !url.pathname.startsWith(cookie.path)) return false; - // TODO: check SameSite. - /* - switch (cookie.getSameSite()) { - case "Strict": - // means that the browser sends the cookie only for same-site requests, that is, requests originating from the same site that set the cookie. If a request originates from a different domain or scheme (even with the same domain), no cookies with the SameSite=Strict attribute are sent. - if (url.hostname !== cookie.getDomain()) return false; - break; - case "Lax": - // means that the cookie is not sent on cross-site requests, such as on requests to load images or frames, but is sent when a user is navigating to the origin site from an external site (for example, when following a link). This is the default behavior if the SameSite attribute is not specified. - if (url.hostname !== cookie.getDomain()) return false; - break; - - case "None": - // means that the browser sends the cookie for same-site requests, and for cross-site requests with a CORS header. This is the default value if the SameSite attribute is not specified. - break; - default: - break; - - } - */ - return true; - }); - return cookies.map(cookie => cookie.cookieString()).join("; "); - } -} \ No newline at end of file + /** + * Get cookie. + * + * @param location Location. + * @param fromDocument If true, the caller is a document. + * @returns Cookie string. + */ + public getCookiesString(location: Location, fromDocument: boolean): string { + const cookies = this.cookies.filter((cookie) => { + // Skip when use document.cookie and the cookie is httponly. + if (fromDocument && cookie.isHttpOnly()) { + return false; + } + if (cookie.isExpired()) { + return false; + } + if (cookie.isSecure() && location.protocol !== 'https:') { + return false; + } + if (cookie.domain && !location.hostname.endsWith(cookie.domain)) { + return false; + } + if (cookie.path && !location.pathname.startsWith(cookie.path)) { + return false; + } + // TODO: check SameSite. + return true; + }); + return cookies.map((cookie) => cookie.cookieString()).join('; '); + } +}