mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-18 09:41:32 -05:00
Compare commits
2 Commits
feat/6908-
...
release/4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a10404ba1d | ||
|
|
39788ce0e1 |
@@ -11,6 +11,7 @@ import {
|
||||
handleHiddenFields,
|
||||
shouldDisplayBasedOnPercentage,
|
||||
} from "@/lib/common/utils";
|
||||
import { UpdateQueue } from "@/lib/user/update-queue";
|
||||
import { type TEnvironmentStateSurvey, type TUserState } from "@/types/config";
|
||||
import { type TTrackProperties } from "@/types/survey";
|
||||
|
||||
@@ -60,6 +61,24 @@ export const renderWidget = async (
|
||||
|
||||
setIsSurveyRunning(true);
|
||||
|
||||
// Wait for pending user identification to complete before rendering
|
||||
const updateQueue = UpdateQueue.getInstance();
|
||||
if (updateQueue.hasPendingWork()) {
|
||||
logger.debug("Waiting for pending user identification before rendering survey");
|
||||
const identificationSucceeded = await updateQueue.waitForPendingWork();
|
||||
if (!identificationSucceeded) {
|
||||
const hasSegmentFilters = Array.isArray(survey.segment?.filters) && survey.segment.filters.length > 0;
|
||||
|
||||
if (hasSegmentFilters) {
|
||||
logger.debug("User identification failed. Skipping survey with segment filters.");
|
||||
setIsSurveyRunning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("User identification failed but survey has no segment filters. Proceeding.");
|
||||
}
|
||||
}
|
||||
|
||||
if (survey.delay) {
|
||||
logger.debug(`Delaying survey "${survey.name}" by ${survey.delay.toString()} seconds.`);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ export class UpdateQueue {
|
||||
private static instance: UpdateQueue | null = null;
|
||||
private updates: TUpdates | null = null;
|
||||
private debounceTimeout: NodeJS.Timeout | null = null;
|
||||
private pendingFlush: Promise<void> | null = null;
|
||||
private readonly DEBOUNCE_DELAY = 500;
|
||||
private readonly PENDING_WORK_TIMEOUT = 5000;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -63,17 +65,45 @@ export class UpdateQueue {
|
||||
return !this.updates;
|
||||
}
|
||||
|
||||
public hasPendingWork(): boolean {
|
||||
return this.updates !== null || this.pendingFlush !== null;
|
||||
}
|
||||
|
||||
public async waitForPendingWork(): Promise<boolean> {
|
||||
if (!this.hasPendingWork()) return true;
|
||||
|
||||
const flush = this.pendingFlush ?? this.processUpdates();
|
||||
try {
|
||||
const succeeded = await Promise.race([
|
||||
flush.then(() => true as const),
|
||||
new Promise<false>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(false);
|
||||
}, this.PENDING_WORK_TIMEOUT);
|
||||
}),
|
||||
]);
|
||||
return succeeded;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async processUpdates(): Promise<void> {
|
||||
const logger = Logger.getInstance();
|
||||
if (!this.updates) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a flush is already in flight, reuse it instead of creating a new promise
|
||||
if (this.pendingFlush) {
|
||||
return this.pendingFlush;
|
||||
}
|
||||
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const flushPromise = new Promise<void>((resolve, reject) => {
|
||||
const handler = async (): Promise<void> => {
|
||||
try {
|
||||
let currentUpdates = { ...this.updates };
|
||||
@@ -147,8 +177,10 @@ export class UpdateQueue {
|
||||
}
|
||||
|
||||
this.clearUpdates();
|
||||
this.pendingFlush = null;
|
||||
resolve();
|
||||
} catch (error: unknown) {
|
||||
this.pendingFlush = null;
|
||||
logger.error(
|
||||
`Failed to process updates: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
);
|
||||
@@ -158,5 +190,8 @@ export class UpdateQueue {
|
||||
|
||||
this.debounceTimeout = setTimeout(() => void handler(), this.DEBOUNCE_DELAY);
|
||||
});
|
||||
|
||||
this.pendingFlush = flushPromise;
|
||||
return flushPromise;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import * as React from "react";
|
||||
import { cn, stripInlineStyles } from "@/lib/utils";
|
||||
|
||||
@@ -39,7 +39,7 @@ function Label({
|
||||
const isHtml = childrenString ? isValidHTML(strippedContent) : false;
|
||||
const safeHtml =
|
||||
isHtml && strippedContent
|
||||
? DOMPurify.sanitize(strippedContent, {
|
||||
? sanitize(strippedContent, {
|
||||
ADD_ATTR: ["target"],
|
||||
FORBID_ATTR: ["style"],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import { extendTailwindMerge } from "tailwind-merge";
|
||||
|
||||
const twMerge = extendTailwindMerge({
|
||||
@@ -27,14 +27,16 @@ export function cn(...inputs: ClassValue[]): string {
|
||||
export const stripInlineStyles = (html: string): string => {
|
||||
if (!html) return html;
|
||||
|
||||
// Use DOMPurify to safely remove style attributes
|
||||
// This is more secure than regex-based approaches and handles edge cases properly
|
||||
return DOMPurify.sanitize(html, {
|
||||
// Pre-strip style attributes from the raw string BEFORE DOMPurify parses it.
|
||||
// DOMPurify internally uses innerHTML to parse HTML, which triggers CSP
|
||||
// `style-src` violations at parse time — before FORBID_ATTR can strip them.
|
||||
// The regex is O(n) safe: [^"]* and [^']* are negated classes bounded by
|
||||
// fixed quote delimiters, so no backtracking can occur.
|
||||
const preStripped = html.replaceAll(/ style="[^"]*"| style='[^']*'/gi, "");
|
||||
|
||||
return sanitize(preStripped, {
|
||||
FORBID_ATTR: ["style"],
|
||||
// Preserve the target attribute (e.g. target="_blank" on links) which is not
|
||||
// in DOMPurify's default allow-list but is explicitly required downstream.
|
||||
ADD_ATTR: ["target"],
|
||||
// Keep other attributes and tags as-is, only remove style attributes
|
||||
KEEP_CONTENT: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"baseUrl": ".",
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020", "ES2021.String"],
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
||||
@@ -10,14 +10,16 @@ import DOMPurify from "isomorphic-dompurify";
|
||||
export const stripInlineStyles = (html: string): string => {
|
||||
if (!html) return html;
|
||||
|
||||
// Use DOMPurify to safely remove style attributes
|
||||
// This is more secure than regex-based approaches and handles edge cases properly
|
||||
return DOMPurify.sanitize(html, {
|
||||
// Pre-strip style attributes from the raw string BEFORE DOMPurify parses it.
|
||||
// DOMPurify internally uses innerHTML to parse HTML, which triggers CSP
|
||||
// `style-src` violations at parse time — before FORBID_ATTR can strip them.
|
||||
// The regex is O(n) safe: [^"]* and [^']* are negated classes bounded by
|
||||
// fixed quote delimiters, so no backtracking can occur.
|
||||
const preStripped = html.replaceAll(/ style="[^"]*"| style='[^']*'/gi, "");
|
||||
|
||||
return DOMPurify.sanitize(preStripped, {
|
||||
FORBID_ATTR: ["style"],
|
||||
// Preserve the target attribute (e.g. target="_blank" on links) which is not
|
||||
// in DOMPurify's default allow-list but is explicitly required downstream.
|
||||
ADD_ATTR: ["target"],
|
||||
// Keep other attributes and tags as-is, only remove style attributes
|
||||
KEEP_CONTENT: true,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user