From 6568df99d68c8af4a00b8ebf910c4536a8c9ebbd Mon Sep 17 00:00:00 2001 From: Max Burri Date: Wed, 9 Sep 2026 09:18:34 +0200 Subject: [PATCH 1/4] fix: do not use dangerouslySetInnerHtml for layer description --- .../map/map-custom-layers-legend.spec.tsx | 19 +++++++++++++++++++ app/charts/map/map-custom-layers-legend.tsx | 19 ++++++++----------- 2 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 app/charts/map/map-custom-layers-legend.spec.tsx diff --git a/app/charts/map/map-custom-layers-legend.spec.tsx b/app/charts/map/map-custom-layers-legend.spec.tsx new file mode 100644 index 0000000000..6a542a6fd7 --- /dev/null +++ b/app/charts/map/map-custom-layers-legend.spec.tsx @@ -0,0 +1,19 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import { CustomLayerDescription } from "@/charts/map/map-custom-layers-legend"; + +afterEach(cleanup); + +describe("CustomLayerDescription", () => { + it("renders WMS layer descriptions as text", () => { + const description = ''; + + const { container } = render( + + ); + + expect(screen.getByText(description)).toBeTruthy(); + expect(container.querySelector("img")).toBeNull(); + }); +}); diff --git a/app/charts/map/map-custom-layers-legend.tsx b/app/charts/map/map-custom-layers-legend.tsx index 39cdce5ec5..5c9564d64b 100644 --- a/app/charts/map/map-custom-layers-legend.tsx +++ b/app/charts/map/map-custom-layers-legend.tsx @@ -1,4 +1,4 @@ -import { Box, Typography, useTheme } from "@mui/material"; +import { Box, Typography } from "@mui/material"; import uniq from "lodash/uniq"; import NextImage from "next/image"; @@ -43,6 +43,12 @@ const constrainSize = ({ return { width, height }; }; +export const CustomLayerDescription = ({ + description, +}: { + description: string; +}) => {description}; + export const MapCustomLayersLegend = ({ chartConfig, value, @@ -52,7 +58,6 @@ export const MapCustomLayersLegend = ({ }) => { const customLayers = chartConfig.baseLayer.customLayers; const { data: legendsData, error } = useLegendsData({ customLayers }); - const theme = useTheme(); return error ? ( {error.message} ) : !legendsData ? ( @@ -99,15 +104,7 @@ export const MapCustomLayersLegend = ({ {layer.description ? ( *": { - // We do not let the tooltip HTML override the font size - fontSize: `${theme.typography.caption.fontSize} !important`, - }, - }} - dangerouslySetInnerHTML={{ __html: layer.description }} - /> + } sx={{ width: "fit-content" }} /> From 48ef155c1dfdbd3e4d4065e1d8c82204afa4bf8f Mon Sep 17 00:00:00 2001 From: Max Burri Date: Wed, 9 Sep 2026 10:32:00 +0200 Subject: [PATCH 2/4] fix: remove setDangerouslyInnerHtml completely --- app/components/dataset-metadata.spec.tsx | 45 ++++++++++++ app/components/dataset-metadata.tsx | 70 ++++++++++++++++-- app/components/debug-search.tsx | 7 +- app/rdf/query-search-score-utils.spec.ts | 25 +++++++ app/rdf/query-search-score-utils.ts | 41 ++++++++++- yarn.lock | 93 +----------------------- 6 files changed, 174 insertions(+), 107 deletions(-) create mode 100644 app/components/dataset-metadata.spec.tsx diff --git a/app/components/dataset-metadata.spec.tsx b/app/components/dataset-metadata.spec.tsx new file mode 100644 index 0000000000..7d1c3f3c67 --- /dev/null +++ b/app/components/dataset-metadata.spec.tsx @@ -0,0 +1,45 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import { DatasetPublisher } from "@/components/dataset-metadata"; + +afterEach(cleanup); + +describe("DatasetPublisher", () => { + it("renders publisher anchor markup as a safe link", () => { + render( + FOEN & BAFU' + } + /> + ); + + const link = screen.getByRole("link", { name: "FOEN & BAFU" }); + expect(link.getAttribute("href")).toBe("https://example.com/?a=1&b=2"); + expect(link.getAttribute("target")).toBe("_blank"); + expect(link.getAttribute("rel")).toBe("noopener noreferrer"); + }); + + it("does not render unsafe publisher URLs as links", () => { + const { container } = render( + Publisher'} + /> + ); + + expect(screen.getByText("Publisher")).toBeTruthy(); + expect(container.querySelector("a")).toBeNull(); + }); + + it("renders plain text and strips unexpected markup", () => { + const { container } = render( + & Office'} + /> + ); + + expect(container.textContent).toBe("Publisher & Office"); + expect(container.querySelector("img")).toBeNull(); + }); +}); diff --git a/app/components/dataset-metadata.tsx b/app/components/dataset-metadata.tsx index d42c2ab503..ddab536a92 100644 --- a/app/components/dataset-metadata.tsx +++ b/app/components/dataset-metadata.tsx @@ -1,7 +1,6 @@ import { sanitizeUrl } from "@braintree/sanitize-url"; import { Trans } from "@lingui/macro"; import { - Box, Link, Link as MUILink, LinkProps, @@ -54,13 +53,7 @@ export const DatasetMetadata = ({ Source - a": { color: "grey.900" } }} - dangerouslySetInnerHTML={{ - __html: cube.publisher, - }} - /> + )} @@ -173,6 +166,67 @@ const DatasetMetadataBody = ({ ); +export const DatasetPublisher = ({ publisher }: { publisher: string }) => { + const { text, href } = parsePublisher(publisher); + + return href ? ( + + {text} + + ) : ( + <>{text} + ); +}; + +const decodeHtmlEntities = (text: string) => { + const namedEntities: Record = { + amp: "&", + apos: "'", + gt: ">", + lt: "<", + quot: '"', + }; + + return text.replace(/&(#(?:x[\da-f]+|\d+)|[a-z]+);/gi, (entity, code) => { + if (code[0] !== "#") { + return namedEntities[code.toLowerCase()] ?? entity; + } + + const value = + code[1].toLowerCase() === "x" + ? parseInt(code.slice(2), 16) + : parseInt(code.slice(1), 10); + return Number.isSafeInteger(value) && value >= 0 && value <= 0x10ffff + ? String.fromCodePoint(value) + : entity; + }); +}; + +const publisherText = (publisher: string) => { + return decodeHtmlEntities(publisher.replace(/<[^>]+>/g, "")); +}; + +const parsePublisher = (publisher: string): { text: string; href?: string } => { + const match = publisher.match( + /]+href=["']([^"']+)["'][^>]*>(.*?)<\/a>/is + ); + + if (match) { + const href = sanitizeUrl(decodeHtmlEntities(match[1])); + const text = publisherText(match[2]); + + return href !== "about:blank" ? { text, href } : { text }; + } + + return { text: publisherText(publisher) }; +}; + const DatasetMetadataLink = ({ href, label, diff --git a/app/components/debug-search.tsx b/app/components/debug-search.tsx index 1ab7d54332..3c9c27b160 100644 --- a/app/components/debug-search.tsx +++ b/app/components/debug-search.tsx @@ -10,10 +10,7 @@ import TextField from "@mui/material/TextField"; import Typography from "@mui/material/Typography"; import { KeyboardEventHandler, useEffect, useRef, useState } from "react"; -import { - SearchCubeFilter, - useSearchCubesQuery, -} from "@/graphql/query-hooks"; +import { SearchCubeFilter, useSearchCubesQuery } from "@/graphql/query-hooks"; import { RequestQueryMeta } from "@/graphql/query-meta"; import { SearchCubeFilterType } from "@/graphql/resolver-types"; @@ -133,7 +130,7 @@ const Search = ({
diff --git a/app/rdf/query-search-score-utils.spec.ts b/app/rdf/query-search-score-utils.spec.ts index 90d319412f..e1ca4eccff 100644 --- a/app/rdf/query-search-score-utils.spec.ts +++ b/app/rdf/query-search-score-utils.spec.ts @@ -22,4 +22,29 @@ describe("highlighting search words in query", () => { expect(result).toEqual(t[2]); } }); + + it("should escape HTML contained in the text", () => { + expect(highlight(' bad', "bad")).toEqual( + "<img src=x onerror="alert(1)"> bad" + ); + }); + + it("should escape HTML contained in the matched part", () => { + expect(highlight("", " after'} + schema={inlineTextSchema} + /> + ); + + expect(container.querySelector("script")).toBeNull(); + expect(container.querySelector("iframe")).toBeNull(); + expect(container.textContent).toContain("Before"); + expect(container.textContent).toContain("after"); + }); + + it("drops event handler attributes", () => { + const { container } = render( + '} + schema={inlineTextSchema} + /> + ); + + expect(container.querySelector("img")).toBeNull(); + expect(container.innerHTML).not.toContain("onerror"); + }); + + it("renders links as safe external links", () => { + render( + FOEN & BAFU'} + schema={inlineTextSchema} + /> + ); + + const link = screen.getByRole("link", { name: "FOEN & BAFU" }); + expect(link.getAttribute("href")).toBe("https://example.com/?a=1&b=2"); + expect(link.getAttribute("target")).toBe("_blank"); + expect(link.getAttribute("rel")).toBe("noopener noreferrer"); + }); + + it("does not render unsafe URLs as links", () => { + const { container } = render( + Publisher'} + schema={inlineTextSchema} + /> + ); + + expect(container.querySelector("a")).toBeNull(); + expect(screen.getByText("Publisher")).toBeTruthy(); + }); + + it("restricts the markup to the given schema", () => { + const { container } = render( + match link'} + schema={boldOnlySchema} + /> + ); + + expect(container.querySelector("b")?.textContent).toBe("match"); + expect(container.querySelector("a")).toBeNull(); + expect(container.textContent).toBe("match link"); + }); + + it("renders on the server", () => { + const markup = renderToStaticMarkup( + ok'} + schema={inlineTextSchema} + /> + ); + + expect(markup).toContain("ok"); + expect(markup).not.toContain("onerror"); + }); +}); diff --git a/app/components/sanitized-html.tsx b/app/components/sanitized-html.tsx new file mode 100644 index 0000000000..6416bc4bb5 --- /dev/null +++ b/app/components/sanitized-html.tsx @@ -0,0 +1,53 @@ +import { sanitizeUrl } from "@braintree/sanitize-url"; +import { Box, BoxProps } from "@mui/material"; +import { fromHtml } from "hast-util-from-html"; +import { sanitize, Schema } from "hast-util-sanitize"; +import { Components, toJsxRuntime } from "hast-util-to-jsx-runtime"; +import { useMemo } from "react"; +import { Fragment, jsx, jsxs } from "react/jsx-runtime"; + +import { inlineTextSchema } from "@/components/sanitize-schema"; + +const components: Partial = { + a: ({ children, href, ...props }) => { + // The schema already restricts the allowed protocols; sanitizing the URL + // again keeps the guarantee even if a caller passes a laxer schema. + const safeHref = href ? sanitizeUrl(href) : undefined; + + return safeHref && safeHref !== "about:blank" ? ( + + {children} + + ) : ( + <>{children} + ); + }, +}; + +/** + * Renders HTML that we do not control (remote cube metadata, capabilities + * documents fetched from third-party endpoints) as React elements, keeping the + * formatting but dropping everything that is not in `schema`. + * + * Prefer this over `dangerouslySetInnerHTML`, so that sanitization happens in a + * single place. Remaining props are forwarded to the wrapping `Box`. + */ +export const SanitizedHtml = ({ + html, + schema = inlineTextSchema, + ...boxProps +}: { + html: string; + schema?: Schema; +} & BoxProps) => { + const content = useMemo(() => { + return toJsxRuntime(sanitize(fromHtml(html, { fragment: true }), schema), { + Fragment, + jsx, + jsxs, + components, + }); + }, [html, schema]); + + return {content}; +}; diff --git a/app/package.json b/app/package.json index cf046c6d1c..5c4962bea0 100644 --- a/app/package.json +++ b/app/package.json @@ -115,6 +115,9 @@ "graphql-constraint-directive": "v2", "graphql-depth-limit": "^1.1.0", "graphql-tag": "^2.12.6", + "hast-util-from-html": "^2.0.3", + "hast-util-sanitize": "^5.0.2", + "hast-util-to-jsx-runtime": "^2.3.2", "html-to-image": "^1.11.11", "iframe-resizer": "^4.2.11", "immer": "^9.0.6", diff --git a/app/rdf/query-search-score-utils.spec.ts b/app/rdf/query-search-score-utils.spec.ts index e1ca4eccff..4962adec01 100644 --- a/app/rdf/query-search-score-utils.spec.ts +++ b/app/rdf/query-search-score-utils.spec.ts @@ -42,6 +42,12 @@ describe("highlighting search words in query", () => { expect(highlight("Pollution is bad", "(")).toEqual("Pollution is bad"); }); + it("should escape HTML when there is nothing to highlight", () => { + expect(highlight('', "")).toEqual( + "<img src=x onerror="alert(1)">" + ); + }); + it("should not highlight empty matches for queries with extra spaces", () => { expect(highlight("Pollution is bad", "is bad")).toEqual( "Pollution is bad" diff --git a/app/rdf/query-search.ts b/app/rdf/query-search.ts index fa7616e840..8323c8fc0c 100644 --- a/app/rdf/query-search.ts +++ b/app/rdf/query-search.ts @@ -147,11 +147,12 @@ export const searchCubes = async ({ ) .map((cube) => ({ cube, - highlightedTitle: query ? highlight(cube.title, query) : cube.title, - highlightedDescription: - query && cube.description - ? highlight(cube.description, query) - : cube.description, + // Always go through highlight, so that the metadata is HTML-escaped even + // when there is nothing to highlight. + highlightedTitle: highlight(cube.title, query ?? ""), + highlightedDescription: cube.description + ? highlight(cube.description, query ?? "") + : cube.description, })); }; diff --git a/yarn.lock b/yarn.lock index d33df08ddc..e794720bae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13342,6 +13342,18 @@ hast-to-hyperscript@^9.0.0: unist-util-is "^4.0.0" web-namespaces "^1.0.0" +hast-util-from-html@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz#485c74785358beb80c4ba6346299311ac4c49c82" + integrity sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.1.0" + hast-util-from-parse5 "^8.0.0" + parse5 "^7.0.0" + vfile "^6.0.0" + vfile-message "^4.0.0" + hast-util-from-parse5@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz" @@ -13354,11 +13366,32 @@ hast-util-from-parse5@^6.0.0: vfile-location "^3.2.0" web-namespaces "^1.0.0" +hast-util-from-parse5@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e" + integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^9.0.0" + property-information "^7.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + hast-util-parse-selector@^2.0.0: version "2.2.5" resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz" integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + hast-util-raw@6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-6.0.1.tgz" @@ -13375,7 +13408,7 @@ hast-util-raw@6.0.1: xtend "^4.0.0" zwitch "^1.0.0" -hast-util-sanitize@^5.0.0: +hast-util-sanitize@^5.0.0, hast-util-sanitize@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz#edb260d94e5bba2030eb9375790a8753e5bf391f" integrity sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg== @@ -13405,6 +13438,27 @@ hast-util-to-jsx-runtime@^2.0.0: unist-util-position "^5.0.0" vfile-message "^4.0.0" +hast-util-to-jsx-runtime@^2.3.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz#ff31897aae59f62232e21594eac7ef6b63333e98" + integrity sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + style-to-js "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + hast-util-to-parse5@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz" @@ -13434,6 +13488,17 @@ hastscript@^6.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" +hastscript@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff" + integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -13764,6 +13829,11 @@ inline-style-parser@0.2.4: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz#f4af5fe72e612839fcd453d989a586566d695f22" integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q== +inline-style-parser@0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz#b1fc68bfc0313b8685745e4464e37f9376b9c909" + integrity sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA== + inquirer@^6.0.0: version "6.5.2" resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz" @@ -17055,7 +17125,7 @@ parse5@^6.0.0: resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== -parse5@^7.2.1: +parse5@^7.0.0, parse5@^7.2.1: version "7.3.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== @@ -17560,6 +17630,11 @@ property-information@^6.0.0: resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== +property-information@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.2.0.tgz#0809b34264e995c0bfcd3227028a1e35210af80a" + integrity sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg== + protobufjs@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-8.0.0.tgz#d884102c1fe8d0b1e2493789ad37bc7ea47c0893" @@ -19757,6 +19832,13 @@ style-mod@^4.0.0, style-mod@^4.1.0: resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67" integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw== +style-to-js@^1.0.0: + version "1.1.21" + resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.21.tgz#2908941187f857e79e28e9cd78008b9a0b3e0e8d" + integrity sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ== + dependencies: + style-to-object "1.0.14" + style-to-object@0.3.0, style-to-object@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz" @@ -19764,6 +19846,13 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" +style-to-object@1.0.14: + version "1.0.14" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.14.tgz#1d22f0e7266bb8c6d8cae5caf4ec4f005e08f611" + integrity sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw== + dependencies: + inline-style-parser "0.2.7" + style-to-object@^1.0.0: version "1.0.8" resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.8.tgz#67a29bca47eaa587db18118d68f9d95955e81292" @@ -20923,6 +21012,14 @@ vfile-location@^3.0.0, vfile-location@^3.2.0: resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz" integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA== +vfile-location@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" + integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + vfile-message@^2.0.0: version "2.0.4" resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz" @@ -21067,6 +21164,11 @@ web-namespaces@^1.0.0: resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + web-streams-polyfill@^3.0.3: version "3.2.1" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" From 7ebf250de7db3b9fce7a58510ef3e04df90ba52c Mon Sep 17 00:00:00 2001 From: Mathis Hofer Date: Fri, 18 Sep 2026 17:42:54 +0200 Subject: [PATCH 4/4] chore: add mise.toml & adjust .env.development to work with mise --- app/.env.development | 2 +- mise.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 mise.toml diff --git a/app/.env.development b/app/.env.development index 01156e3802..6fd0ca187c 100644 --- a/app/.env.development +++ b/app/.env.development @@ -2,7 +2,7 @@ DATABASE_URL=postgres://postgres:password@localhost:5432/visualization_tool ENDPOINT=sparql+https://cached.lindas.admin.ch/query SPARQL_GEO_ENDPOINT=https://geo.ld.admin.ch/query GRAPHQL_ENDPOINT=/api/graphql -WHITELISTED_DATA_SOURCES=["Prod", "Prod-uncached", "Int", "Int-uncached", "Test", "Test-uncached"] +WHITELISTED_DATA_SOURCES='["Prod", "Prod-uncached", "Int", "Int-uncached", "Test", "Test-uncached"]' SENTRY_IGNORE_API_RESOLUTION_ERROR=1 MAPTILER_API_KEY=123 ADFS_PROFILE_URL=https://www.myaccount-r.eiam.admin.ch/ diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..5a4d8bcb6d --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[env] +_.file = "app/.env.development"