UNPKG

@vaadin/hilla-frontend

Version:

Hilla core frontend utils

183 lines 5.57 kB
import CookieManager from "./CookieManager.js"; export let CsrfInfoType = function(CsrfInfoType) { CsrfInfoType["SPRING"] = "Spring"; CsrfInfoType["VAADIN"] = "Vaadin"; return CsrfInfoType; }({}); function isCsrfInfo(o) { return o !== null && typeof o === "object" && "headerEntries" in o && "formDataEntries" in o && "timestamp" in o; } /** @internal */ export const VAADIN_CSRF_HEADER = "X-CSRF-Token"; /** @internal */ export const VAADIN_CSRF_COOKIE_NAME = "csrfToken"; /** @internal */ export const SPRING_CSRF_COOKIE_NAME = "XSRF-TOKEN"; function extractContentFromMetaTag(doc, metaTag) { const element = doc.head.querySelector(`meta[name="${metaTag}"]`); const value = element?.content; if (value && value.toLowerCase() !== "undefined") { return value; } return undefined; } function updateMetaTag(doc, name, content) { const meta = doc.createElement("meta"); meta.name = name; meta.content = content; const existing = doc.head.querySelector(`meta[name="${name}"]`); if (existing) { existing.replaceWith(meta); } else { doc.head.appendChild(meta); } } export function clearCsrfInfoMeta(doc) { Array.from(doc.head.querySelectorAll("meta[name=\"_csrf\"], meta[name=\"_csrf_header\"], meta[name=\"_csrf_parameter\"]")).forEach((el) => el.remove()); } export function updateCsrfInfoMeta(csrfInfo, doc) { if (csrfInfo.type !== CsrfInfoType.SPRING) { return; } if (csrfInfo.headerEntries.length > 0) { const [[csrfHeader, csrf]] = csrfInfo.headerEntries; updateMetaTag(doc, "_csrf_header", csrfHeader); updateMetaTag(doc, "_csrf", csrf); } if (csrfInfo.formDataEntries.length > 0) { const [[csrfParameter]] = csrfInfo.formDataEntries; updateMetaTag(doc, "_csrf_parameter", csrfParameter); } } /** @internal */ export async function extractCsrfInfoFromMeta(doc) { const timestamp = Date.now(); const springCsrf = await Promise.resolve().then(() => CookieManager.get(SPRING_CSRF_COOKIE_NAME)) ?? extractContentFromMetaTag(doc, "_csrf"); if (springCsrf) { const csrfHeader = extractContentFromMetaTag(doc, "_csrf_header"); const csrfParameter = extractContentFromMetaTag(doc, "_csrf_parameter"); return { headerEntries: csrfHeader ? [[csrfHeader, springCsrf]] : [], formDataEntries: csrfParameter ? [[csrfParameter, springCsrf]] : [], timestamp, type: CsrfInfoType.SPRING }; } const vaadinCsrf = CookieManager.get(VAADIN_CSRF_COOKIE_NAME) ?? ""; return { type: CsrfInfoType.VAADIN, headerEntries: [[VAADIN_CSRF_HEADER, vaadinCsrf]], formDataEntries: [], timestamp }; } /** @internal */ export class SharedCsrfInfoSource { #updateChannel; #requestUpdateChannel; #valuePromise; #resolveInitialValue; #lastUpdateTimestamp = 0; constructor() { this.reset(); } open() { if (this.#updateChannel || this.#requestUpdateChannel) { this.close(); } this.#updateChannel = new BroadcastChannel(this.#getBroadcastChannelName("update")); this.#updateChannel.onmessage = (e) => { if (!isCsrfInfo(e.data)) { return; } const csrfInfo = e.data; if (csrfInfo.timestamp > this.#lastUpdateTimestamp) { this.#lastUpdateTimestamp = csrfInfo.timestamp; this.#receiveCsrfInfo(csrfInfo); } }; this.#requestUpdateChannel = new BroadcastChannel(this.#getBroadcastChannelName("requestUpdate")); this.#requestUpdateChannel.onmessage = () => { this.get().then((csrfInfo) => { this.#sendCsrfInfo(csrfInfo); }, console.error); }; if (this.#lastUpdateTimestamp > 0) { this.#requestUpdateChannel.postMessage(undefined); } } close() { if (this.#requestUpdateChannel) { this.#requestUpdateChannel.onmessage = null; this.#requestUpdateChannel.close(); this.#requestUpdateChannel = undefined; } if (this.#updateChannel) { this.#updateChannel.onmessage = null; this.#updateChannel.close(); this.#updateChannel = undefined; } } #getBroadcastChannelName(name) { return `@vaadin/hilla-frontend/SharedCsrfUtils.${name}`; } async get() { return this.#valuePromise; } reset() { this.#lastUpdateTimestamp = 0; this.close(); this.open(); this.#valuePromise = this._getInitial().then((csrfInfo) => { this.#lastUpdateTimestamp = csrfInfo.timestamp; return csrfInfo; }); if (!this.#resolveInitialValue) { this.get().then((csrfInfo) => { this.#sendCsrfInfo(csrfInfo); }).catch(console.error); } } /** * Provides initial value for both constructor and `reset()`. The default * implementation uses messages to get a shared value from another window * or worker client. */ async _getInitial() { return new Promise((resolve) => { this.#resolveInitialValue = resolve; this.#requestUpdateChannel?.postMessage(undefined); }); } #sendCsrfInfo(csrfInfo) { this.#updateChannel?.postMessage(csrfInfo); } #receiveCsrfInfo(csrfInfo) { if (this.#resolveInitialValue) { this.#resolveInitialValue(csrfInfo); this.#resolveInitialValue = undefined; } else { this.#valuePromise = Promise.resolve(csrfInfo); } } } /** @internal */ export class BrowserCsrfInfoSource extends SharedCsrfInfoSource { constructor() { super(); globalThis.addEventListener("pagehide", this.close.bind(this)); globalThis.addEventListener("pageshow", this.open.bind(this)); } async _getInitial() { return extractCsrfInfoFromMeta(globalThis.document); } } /** @internal */ let csrfInfoSource; if (globalThis.document) { csrfInfoSource = new BrowserCsrfInfoSource(); } else { csrfInfoSource = new SharedCsrfInfoSource(); } export default csrfInfoSource; //# sourceMappingURL=./CsrfInfoSource.js.map