mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Same PR as #6396, just with the conflicts resolved and some fixes on top. Original commit by @saul1310 is preserved as-is; everything else is a follow-up commit. Refs #6272 ## Conflicts #6396 was written before the frontend was restructured, so all four files it touched moved (`frontend/src/**` -> `frontend/editor/src/**`) and `LinkLayer.tsx` had drifted. Cherry-picked with rename detection and re-resolved against current `main`. ## Fixes on top - **Reuse the existing platform seam instead of adding a second one.** `main` already has `@app/platform/*` seams with per-flavour implementations; #6396 added a parallel `@app/utils/openExternalUrl` core+desktop pair that re-implemented the Tauri shell call already in `desktop/platform/openExternal.ts`. Split into a pure sanitiser (`@app/utils/externalUrl`) and a platform seam (`@app/platform/openExternalTab`), with the desktop impl delegating to the existing `openExternal`. - **Kept PDF links off the `openExternal` seam.** That seam is for leave-and-return redirects (Stripe) and its saas impl is `window.location.assign` - routing PDF links through it would navigate the whole app away from the user's document. `openExternalTab` always opens alongside the app; desktop shadows it to escape the webview. - **Fixed the same defect in two sibling call sites** that #6396 didn't cover: `BookmarkSidebar` (bookmark URI / LaunchAppOrOpenFile actions) and `useAnnotationMenuHandlers` (annotation menu "go to link"). Both called `window.open` on an unsanitised PDF-supplied URI, so on desktop they trapped the link in the webview exactly like the viewer did. - **Dropped the unguarded fallback.** The old code fell back to `window.open(uri)` when `new URL()` threw, so an unparseable URI bypassed the allowlist entirely. It is now blocked. - Empty/whitespace URIs are blocked rather than silently resolving to the app's own page via the base URL. - Tests: sanitiser cases (casing, leading whitespace, `data:`, `vbscript:`, unparseable), a core seam test asserting new-tab-not-navigate, and a desktop seam regression test asserting the URL goes to the OS rather than `window.open`. - **`openExternalTab` now re-validates its own input.** Every caller sanitises first, so nothing reached it unvalidated - but it is the sink that hands a URL to `window.open` (executes `javascript:` in our origin) or to an OS handler on desktop, and its safety shouldn't depend on callers remembering. Both impls fail closed, with tests that call them directly with `javascript:`/`data:`/`file:`/`ftp:`. ## Unrelated fix included (flagged deliberately) The last commit fixes `frontend/editor/vitest.config.ts`: `testTimeout: 10000` was set on the root `test` block, but tests all run under `projects`, which do not inherit it - so the whole suite has silently been running at vitest's 5s default. This is not cosmetic. It made `task check` fail intermittently on unrelated portal specs (`demoData`, `ConnectionModal`); the ConsignO test takes 2966ms with only the portal project running, i.e. 59% of a budget it was never meant to have, so any CPU contention tips it over. Proven with an identical 6.5s probe test: times out at 5000ms on the old config, passes at 6512ms on the fixed one. Happy to split this into its own PR if preferred - it is here because the gate could not be trusted without it. ## Validation Typecheck passes for all 7 build flavours (core, proprietary, saas, desktop, cloud, prototypes, portal); ESLint, Prettier, dpdm and the full 1662-test vitest suite pass. Driven live against the dev server + backend with a PDF carrying five URI annotations (https, `javascript:`, mailto, `file:`, relative). 14/14 behavioural checks pass on this branch; 5 of them fail on `main`: | check | main | this PR | | --- | --- | --- | | safe https link exposes real href (copy-link) | `href="#"` | `https://example.com/safe-link?a=1` | | link opens in new tab / tabnabbing-proof | no `target`/`rel` | `_blank` + `noopener noreferrer` | | mailto link exposes real href | `href="#"` | `mailto:test@example.com` | | relative URI resolved against app origin | `href="#"` | resolved | | `javascript:` / `file:` never reach href | blocked | blocked | | clicking blocked link doesn't execute or navigate | ok | ok | | clicking safe link opens new tab at source URL | - | ok, app not navigated away | --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Testing (if applicable) - [x] I have run `task check` to verify linters, typechecks, and tests pass - [x] I have tested my changes locally --------- Co-authored-by: Saul <saulifshin.cs@gmail.com>
162 lines
4.2 KiB
TypeScript
162 lines
4.2 KiB
TypeScript
import { defineConfig } from "vitest/config";
|
|
import react from "@vitejs/plugin-react-swc";
|
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
|
|
// Projects do NOT inherit the root test.testTimeout, so every project silently
|
|
// ran at vitest's 5s default. Spread this into each one instead.
|
|
const TIMEOUTS = { testTimeout: 10000, hookTimeout: 10000 };
|
|
|
|
export default defineConfig({
|
|
test: {
|
|
globals: true,
|
|
environment: "jsdom",
|
|
setupFiles: ["./src/core/setupTests.ts"],
|
|
css: false,
|
|
exclude: [
|
|
"node_modules/",
|
|
"src/**/*.spec.ts", // Exclude Playwright E2E tests
|
|
"src/tests/test-fixtures/**",
|
|
],
|
|
...TIMEOUTS,
|
|
coverage: {
|
|
reporter: ["text", "json", "html"],
|
|
exclude: [
|
|
"node_modules/",
|
|
"src/core/setupTests.ts",
|
|
"src/proprietary/setupTests.ts",
|
|
"src/saas/setupTests.ts",
|
|
"**/*.d.ts",
|
|
"src/tests/test-fixtures/**",
|
|
"src/**/*.spec.ts",
|
|
],
|
|
},
|
|
projects: [
|
|
{
|
|
test: {
|
|
name: "core",
|
|
...TIMEOUTS,
|
|
include: ["src/core/**/*.test.{ts,tsx}"],
|
|
environment: "jsdom",
|
|
globals: true,
|
|
setupFiles: ["./src/core/setupTests.ts"],
|
|
},
|
|
plugins: [
|
|
react(),
|
|
tsconfigPaths({
|
|
projects: ["./tsconfig.core.vite.json"],
|
|
}),
|
|
],
|
|
esbuild: {
|
|
target: "es2020",
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: "portal",
|
|
...TIMEOUTS,
|
|
include: ["src/portal/**/*.test.{ts,tsx}"],
|
|
environment: "jsdom",
|
|
globals: true,
|
|
setupFiles: ["./src/portal/setupTests.ts"],
|
|
},
|
|
plugins: [
|
|
react(),
|
|
tsconfigPaths({
|
|
// Broad project so @app/@portal resolve in every editor file the
|
|
// portal tests pull in (core/ui, core, ...).
|
|
projects: ["./tsconfig.portal.vite.json"],
|
|
}),
|
|
],
|
|
esbuild: {
|
|
target: "es2020",
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: "proprietary",
|
|
...TIMEOUTS,
|
|
include: ["src/proprietary/**/*.test.{ts,tsx}"],
|
|
environment: "jsdom",
|
|
globals: true,
|
|
setupFiles: ["./src/core/setupTests.ts"],
|
|
},
|
|
plugins: [
|
|
react(),
|
|
tsconfigPaths({
|
|
projects: ["./tsconfig.proprietary.vite.json"],
|
|
}),
|
|
],
|
|
esbuild: {
|
|
target: "es2020",
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: "desktop",
|
|
...TIMEOUTS,
|
|
include: ["src/desktop/**/*.test.{ts,tsx}"],
|
|
environment: "jsdom",
|
|
globals: true,
|
|
setupFiles: ["./src/core/setupTests.ts"],
|
|
},
|
|
plugins: [
|
|
react(),
|
|
tsconfigPaths({
|
|
projects: ["./tsconfig.desktop.vite.json"],
|
|
}),
|
|
],
|
|
esbuild: {
|
|
target: "es2020",
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: "saas",
|
|
...TIMEOUTS,
|
|
// src/saas = editor-saas layer; src/portal-saas = the portal's saas
|
|
// overrides (sibling to src/portal). Both build under the saas flavor,
|
|
// so both resolve @portal via the saas cascade (tsconfig.saas.vite.json).
|
|
include: [
|
|
"src/saas/**/*.test.{ts,tsx}",
|
|
"src/portal-saas/**/*.test.{ts,tsx}",
|
|
],
|
|
environment: "jsdom",
|
|
globals: true,
|
|
setupFiles: ["./src/saas/setupTests.ts"],
|
|
},
|
|
plugins: [
|
|
react(),
|
|
tsconfigPaths({
|
|
projects: ["./tsconfig.saas.vite.json"],
|
|
}),
|
|
],
|
|
esbuild: {
|
|
target: "es2020",
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: "prototypes",
|
|
...TIMEOUTS,
|
|
include: ["src/prototypes/**/*.test.{ts,tsx}"],
|
|
environment: "jsdom",
|
|
globals: true,
|
|
setupFiles: ["./src/core/setupTests.ts"],
|
|
},
|
|
plugins: [
|
|
react(),
|
|
tsconfigPaths({
|
|
projects: ["./tsconfig.prototypes.vite.json"],
|
|
}),
|
|
],
|
|
esbuild: {
|
|
target: "es2020",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
esbuild: {
|
|
target: "es2020",
|
|
},
|
|
});
|