Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { AppConnectionPage } from "@/modules/workspaces/settings/(setup)/app-connection/page";

export const generateMetadata = () => getSettingsPageMetadata("common.connect_your_app");

export default AppConnectionPage;
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";

export const generateMetadata = () => getSettingsPageMetadata("common.user_actions");

export { UserActionsPage as default } from "@/modules/workspaces/settings/(setup)/user-actions/page";
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { GeneralSettingsPage } from "@/modules/workspaces/settings/general/page";

export const generateMetadata = () => getSettingsPageMetadata("common.workspace_settings");

export default GeneralSettingsPage;
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ import { redactIntegrationCredentials } from "@/lib/integration/redact-credentia
import { getIntegrations } from "@/lib/integration/service";
import { getUserLocale } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";

export const generateMetadata = () =>
getSettingsPageMetadata("workspace.integrations.airtable.airtable_integration");

const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
const params = await props.params;
const t = await getTranslate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ import { redactIntegrationCredentials } from "@/lib/integration/redact-credentia
import { getIntegrations } from "@/lib/integration/service";
import { getUserLocale } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";

export const generateMetadata = () =>
getSettingsPageMetadata("workspace.integrations.google_sheets.google_sheets_integration");

const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
const params = await props.params;
const t = await getTranslate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ import { getNotionDatabases } from "@/lib/notion/service";
import { getUserLocale } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";

export const generateMetadata = () =>
getSettingsPageMetadata("workspace.integrations.notion.notion_integration");

const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
const params = await props.params;
const t = await getTranslate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getIntegrations } from "@/lib/integration/service";
import { getBillingFallbackPath } from "@/lib/membership/navigation";
import { getTranslate } from "@/lingodotdev/server";
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { Card } from "@/modules/ui/components/integration-card";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";

export const generateMetadata = () => getSettingsPageMetadata("common.integrations");

const getStatusText = (count: number, t: TFunction, type: string) => {
if (count === 1) return `1 ${type}`;
if (count === 0) return t("common.not_connected");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import { redactIntegrationCredentials } from "@/lib/integration/redact-credentia
import { getIntegrationByType } from "@/lib/integration/service";
import { getUserLocale } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";

export const generateMetadata = () =>
getSettingsPageMetadata("workspace.integrations.slack.slack_integration");

const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
const params = await props.params;
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { WebhooksPage } from "@/modules/integrations/webhooks/page";
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";

export const generateMetadata = () => getSettingsPageMetadata("common.webhooks");

export default WebhooksPage;
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { LanguagesPage } from "@/modules/workspaces/settings/languages/page";

export const generateMetadata = () => getSettingsPageMetadata("common.survey_languages");

export default LanguagesPage;
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { WorkspaceSettingsLayout, metadata } from "@/modules/workspaces/settings/layout";
import { WorkspaceSettingsLayout } from "@/modules/workspaces/settings/layout";

export { metadata };
export default WorkspaceSettingsLayout;
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { WorkspaceLookSettingsPage } from "@/modules/workspaces/settings/look/page";

export const generateMetadata = () => getSettingsPageMetadata("common.appearance");

export default WorkspaceLookSettingsPage;
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";
import { TagsPage } from "@/modules/workspaces/settings/tags/page";

export const generateMetadata = () => getSettingsPageMetadata("common.tags");

export default TagsPage;
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { WorkspaceTeams } from "@/modules/ee/teams/workspace-teams/page";
import { getSettingsPageMetadata } from "@/modules/settings/lib/metadata";

export const generateMetadata = () => getSettingsPageMetadata("common.team_access");

export default WorkspaceTeams;
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
const EMAIL_DOCTYPE_PATTERN = /<!DOCTYPE[^>]*>/i;
const EMAIL_BODY_PATTERN = /<body\b[^>]*>([\s\S]*?)<\/body>/i;
import { findClosingTag, findOpeningTag } from "@/lib/utils/html-opening-tag";

const EMAIL_REACT_SERVER_MARKER_PATTERN = /<!--\/?\$-->/g;

/**
* The body content, or null when the document has no `<body>…</body>`.
*
* Two scans rather than `<body\b[^>]*>([\s\S]*?)<\/body>`. That pattern is quadratic twice over: the
* attribute run rescans from every `<body` when no `>` follows, and the lazy content group expands
* to the end of the document once per `<body>` when no `</body>` follows. Capping fixes neither —
* the content group is the whole email and cannot be capped, and capping the attribute run makes an
* over-long tag match a LATER `<body>` instead, extracting the wrong span.
*
* Same result as the regex. It matched the leftmost `<body…>` that has a `</body>` after it, and if
* none follows the first opening tag then none follows a later one either, so taking the first
* opening tag and the first close after it picks exactly the same span.
*/
const extractBodyContent = (html: string): string | null => {
const openTag = findOpeningTag(html, "body");
if (!openTag) return null;

const contentStart = openTag.index + openTag.length;
const contentEnd = findClosingTag(html, "body", contentStart);

return contentEnd === -1 ? null : html.slice(contentStart, contentEnd);
};

export const extractEmailBodyFragment = (html: string): string => {
const htmlWithoutDoctype = html.replace(EMAIL_DOCTYPE_PATTERN, "").trim();
const bodyMatch = EMAIL_BODY_PATTERN.exec(htmlWithoutDoctype);
const fragment = bodyMatch?.[1].trim() ?? htmlWithoutDoctype;
const doctype = findOpeningTag(html, "!DOCTYPE", { requireWordBoundary: false });
const htmlWithoutDoctype = (
doctype ? html.slice(0, doctype.index) + html.slice(doctype.index + doctype.length) : html
).trim();

const fragment = extractBodyContent(htmlWithoutDoctype)?.trim() ?? htmlWithoutDoctype;

return fragment.replaceAll(EMAIL_REACT_SERVER_MARKER_PATTERN, "").trim();
};
13 changes: 3 additions & 10 deletions apps/web/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";

const AppLayout = async ({ children }: { children: React.ReactNode }) => {
return (
<>
<NoMobileOverlay />
{children}
</>
);
const AuthGroupLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return <>{children}</>;
};

export default AppLayout;
export default AuthGroupLayout;
5 changes: 5 additions & 0 deletions apps/web/i18n.lock
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ checksums:
auth/signup/company_email_required: 997bf11a31512cedf4ab8acb7b4f56c3
auth/signup/have_an_account: 6f9c2d441e93cb6df9ef7dc898e80e05
auth/signup/log_in: 9fb886eff8d1d67d8bb79f5c3f78edde
auth/signup/password_requirements: 58d15c4e16b28c51b83977d0c2af17bc
auth/signup/password_validation_contain_at_least_1_number: 58fb33a486aa4dadfc05e3242fc7ea0b
auth/signup/password_validation_minimum_8_and_maximum_128_characters: ec704690a454c7f01e59856c39206259
auth/signup/password_validation_uppercase_and_lowercase: ae98b485024dbff1022f6048e22443cd
Expand Down Expand Up @@ -230,6 +231,7 @@ checksums:
common/delete: 8bcf303dd10a645b5baacb02b47d72c9
common/delete_what: 718ddfcc1dec7f3e8b67856fba838267
common/description: e17686a22ffad04cc7bb70524ed4478b
common/digit_number_of_total: bd36d550b8797c5a928ede29cb10a0f9
common/disable: 81b754fd7962e0bd9b6ba87f3972e7fc
common/disabled: 0889a3dfd914a7ef638611796b17bf72
common/disallow: 01c8ed3ce545ed836d3ccffc562c8a0c
Expand Down Expand Up @@ -274,6 +276,7 @@ checksums:
common/finish: ffa7a10f71182b48fefed7135bee24fa
common/finished_at: 05d2fa31cf3b2e2255729ec7898240c2
common/first_name: cf040a5d6a9fd696be400380cc99f54b
common/formbricks_homepage: 95a78e9e1812a97221d1f69015561e68
common/formbricks_version: d9967c797f3e49ca0cae78bc0ebd19cb
common/full_name: f45991923345e8322c9ff8cd6b7e2b16
common/gathering_responses: c5914490ed81bd77f13d411739f0c9ef
Expand All @@ -286,6 +289,7 @@ checksums:
common/hidden_field: 3ed5c58d0ed359e558cdf7bd33606d2d
common/hidden_fields: 3de6cfd308293a826cb8679fd1d49972
common/hide_column: 23ce94db148f2d8e4a0923defead6cf1
common/hide_password: dd9813264cfc4a7ae515cd5644943c1b
common/html: f750870203043349d570d8f5865ca0f8
common/id: c8886d38aeea2ed5f785aba4fc96784b
common/image: 048ba7a239de0fbd883ade8558415830
Expand Down Expand Up @@ -455,6 +459,7 @@ checksums:
common/settings: 8df6777277469c1fd88cc18dde2f1cc3
common/share_feedback: f3c14bfa149fde4035b6965cb8d3e993
common/show: 16dfe5dc481240cd2819a6394f90df92
common/show_password: 8696d19a0f02613a86727810355fef47
common/show_response_count: 609e5dc7c074d57e711a728fa2f8eb79
common/shown: 63e4ffb245c05e04b636446c3dbdd8df
common/size: 227fadeeff951e041ff42031a11a4626
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/utils/client-ip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const UNTRUSTED_CLIENT_IP = "untrusted-client-ip";

const CLIENT_IP_WARNING_INTERVAL_MS = 10 * 60 * 1000;
const VALID_DECIMAL_PORT = /^[1-9]\d{0,4}$/;
const BRACKETED_ADDRESS = /^\[([^\[\]]+)\](?::([^:]+))?$/;
const BRACKETED_ADDRESS = /^\[([^[\]]+)\](?::([^:]+))?$/;
const IPV4_SOCKET = /^([^:]+):(\d+)$/;

type ClientIpWarningReason = "disabled" | "invalid-selected-hop" | "missing-chain" | "short-chain";
Expand Down
109 changes: 109 additions & 0 deletions apps/web/lib/utils/html-opening-tag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, expect, test } from "vitest";
import { findClosingTag, findOpeningTag, replaceOpeningTags } from "./html-opening-tag";

// The regexes this module replaces, kept here as the oracle every case is compared against.
const asRegex = (name: string, wordBoundary = true) =>
new RegExp(`<${name}${wordBoundary ? String.raw`\b` : ""}([^>]*)>`, "gi");

const CASES: { name: string; wordBoundary?: boolean }[] = [
{ name: "p" },
{ name: "li" },
{ name: "body" },
{ name: "!DOCTYPE", wordBoundary: false },
];

const FIXED = [
"",
"<p>a</p>",
'<p style="margin:0" class="x">a</p>',
"<p >spaced</p><P>upper</P>",
"<pre>not a p</pre>",
"<p",
"<p unclosed",
"<li><p>nested</p></li>",
"<!DOCTYPE html><body>x</body>",
"<!DOCTYPEhtml>",
"<body\ndata-x='1'\n>multiline",
"<p a><p b><p c>",
"text with no tags at all",
"<p></p><p></p>",
"<p_underscore>",
// The shapes a length cap got wrong: an over-long run containing another opening tag.
`<p ${"a".repeat(5000)}<p style="x">tail`,
`<li ${"a".repeat(5000)}<li style="x">tail`,
`<body ${"a".repeat(5000)}<body class="x">inner`,
`<!DOCTYPE ${"a".repeat(5000)}<!DOCTYPE html>rest`,
// U+0130 lowercases to two code units; a toLowerCase()-based scan would misalign here.
"İ<p style='x'>after a length-changing character</p>",
];

const ALPHABET = "<>/plibodyPLIBODY!DCTYPE \n\t\"'=-0_";
const random = Array.from({ length: 20000 }, () => {
const n = Math.floor(Math.random() * 40);
let s = "";
for (let i = 0; i < n; i++) s += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
return s;
});
const CORPUS = [...FIXED, ...random];

describe("html opening tag scanner", () => {
test.each(CASES)("replaceOpeningTags matches the regex it replaces ($name)", ({ name, wordBoundary }) => {
for (const source of CORPUS) {
const viaRegex = source.replace(asRegex(name, wordBoundary), (_m, attrs: string) => `[${attrs}]`);
const viaScan = replaceOpeningTags(source, name, (attrs) => `[${attrs}]`, {
requireWordBoundary: wordBoundary,
});

expect(viaScan, `input: ${JSON.stringify(source.slice(0, 60))}`).toBe(viaRegex);
}
});

test.each(CASES)(
"findOpeningTag reports the regex's index and capture ($name)",
({ name, wordBoundary }) => {
for (const source of CORPUS) {
const expected = asRegex(name, wordBoundary).exec(source);
const actual = findOpeningTag(source, name, { requireWordBoundary: wordBoundary });

if (expected === null) {
expect(actual, `input: ${JSON.stringify(source.slice(0, 60))}`).toBeNull();
continue;
}
expect(actual, `input: ${JSON.stringify(source.slice(0, 60))}`).toEqual({
index: expected.index,
length: expected[0].length,
attributes: expected[1],
});
}
}
);

test("findClosingTag matches the case-insensitive close the regex used", () => {
const iDot = String.fromCharCode(0x130); // lowercases to two code units
const cases = [
["</body>", 0],
["<body>x</BODY>", 7],
["<body>x</BoDy>rest", 7],
["no close here", -1],
// A toLowerCase()-based search would report an index into a longer string here.
[`<BODY>prefix${iDot}suffix</BoDy>`, `<BODY>prefix${iDot}suffix`.length],
[`${iDot.repeat(20)}</body>`, 20],
] as const;

for (const [source, expected] of cases) {
expect(findClosingTag(source, "body"), JSON.stringify(source)).toBe(expected);
}
});

test("stays linear where the regex was quadratic", () => {
// `<p ` repeated with no `>` anywhere: the regex rescans to the end from every occurrence.
const pathological = "<p ".repeat(70000);

const startedAt = performance.now();
const result = replaceOpeningTags(pathological, "p", (attrs) => `[${attrs}]`);
const elapsedMs = performance.now() - startedAt;

expect(result).toBe(pathological);
expect(elapsedMs).toBeLessThan(500);
});
});
Loading
Loading