@vaadin/hilla-frontend
Version:
Hilla core frontend utils
183 lines • 5.57 kB
JavaScript
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 `/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