From 9143f3548266396233d5d3a4e207ac543f457506 Mon Sep 17 00:00:00 2001 From: Arthur Le Meur Date: Tue, 22 Sep 2026 12:07:39 +0200 Subject: [PATCH 01/11] update from dialog to pages for profile screens --- .../customization/Customization.tsx | 87 +++++ .../components/layout/LibraryHeader.tsx | 112 +----- src/renderer/library/customization/route.ts | 20 + src/renderer/library/routing.ts | 6 + .../screen/faq-en.html | 369 +++++++++++++++--- .../screen/faq-fr.html | 366 ++++++++++++++--- 6 files changed, 752 insertions(+), 208 deletions(-) create mode 100644 src/renderer/library/components/customization/Customization.tsx create mode 100644 src/renderer/library/customization/route.ts diff --git a/src/renderer/library/components/customization/Customization.tsx b/src/renderer/library/components/customization/Customization.tsx new file mode 100644 index 0000000000..435a276857 --- /dev/null +++ b/src/renderer/library/components/customization/Customization.tsx @@ -0,0 +1,87 @@ +// ==LICENSE-BEGIN== +// Copyright 2017 European Digital Reading Lab. All rights reserved. +// Licensed to the Readium Foundation under one or more contributor license agreements. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file exposed on Github (readium) in the project repository. +// ==LICENSE-END== + +import * as React from "react"; +import DOMPurify from "dompurify"; +import { useParams } from "react-router-dom"; + +import LibraryLayout from "../layout/LibraryLayout"; +import PublicationAddButton from "../catalog/PublicationAddButton"; +import { useTranslator } from "readium-desktop/renderer/common/hooks/useTranslator"; +import { useSelector } from "readium-desktop/renderer/common/hooks/useSelector"; +import { ILibraryRootState } from "readium-desktop/common/redux/states/renderer/libraryRootState"; +import { convertMultiLangStringToString } from "readium-desktop/common/language-string"; +import { decodeCustomizationRouteParam } from "../../customization/route"; +import { encodeURIComponent_RFC3986 } from "@r2-utils-js/_utils/http/UrlUtils"; +import { URL_PROTOCOL_THORIUMHTTPS, URL_HOST_COMMON, URL_PATH_PREFIX_CUSTOMPROFILEZIP } from "readium-desktop/common/streamerProtocol"; + +const secondaryHeader = ; + +const CustomizationPage = () => { + const [__] = useTranslator(); + const { hrefEncoded } = useParams<{ hrefEncoded: string }>(); + + const customizationManifest = useSelector((state: ILibraryRootState) => state.customization.manifest); + const locale = useSelector((state: ILibraryRootState) => state.i18n.locale); + const customizationId = customizationManifest?.identifier; + const customizationBaseUrl = customizationId ? `${URL_PROTOCOL_THORIUMHTTPS}://${URL_HOST_COMMON}/${URL_PATH_PREFIX_CUSTOMPROFILEZIP}/${encodeURIComponent_RFC3986(Buffer.from(customizationId).toString("base64"))}/` : ""; + + const href = hrefEncoded ? decodeCustomizationRouteParam(hrefEncoded) : undefined; + const screenLink = customizationManifest?.links?.find((ln) => ln.rel === "screen" && ln.href === href); + const title = convertMultiLangStringToString(screenLink?.title, locale) || __("catalog.customization.fallback.screen"); + + const [dangerousInnerHTML_CustomProfileScreenSanitized, setDangerousInnerHtml] = React.useState(undefined); + + React.useEffect(() => { + let cancelled = false; + setDangerousInnerHtml(undefined); + + if (href && customizationBaseUrl) { + // URL is thoriumhttps:// "custom-profile-zip" protocol handler, so no use of isURL(url) and /^https?:\/\//.test(url) checks here + const url = customizationBaseUrl + encodeURIComponent_RFC3986(Buffer.from(href).toString("base64")); + + fetch(url) + .then((response) => { + if (response.ok) { + return response.text(); + } + return Promise.reject(response.statusText); + }) + .then((rawHtmlContent) => { + if (cancelled || !rawHtmlContent) { + return; + } + + const htmlSanitized = DOMPurify.sanitize(rawHtmlContent, { FORBID_TAGS: [/*"style"*/], FORBID_ATTR: [/*"style"*/] /* TODO: handle external https links */ }); + // NOTE that xxx is fine, caught by webContents.on("will-navigate", ...) with event.preventDefault() and shell.openExternal(...) on normalized/escaped URL and filtered on HTTP(S):// + // NOTE that the attribute target="_blank" (etc) is automatically removed by DOMPurify but would be caught by webContents.setWindowOpenHandler(...) with { action: "deny" }, although no shell.openExternal(...) in this case + setDangerousInnerHtml(htmlSanitized); + }) + .catch((e) => { + console.error("Error fetching data:", e); + }); + } + + return () => { + cancelled = true; + }; + }, [href, customizationBaseUrl]); + + return ( + + { + dangerousInnerHTML_CustomProfileScreenSanitized ? +
: <> + } + + ); +}; + +export default CustomizationPage; diff --git a/src/renderer/library/components/layout/LibraryHeader.tsx b/src/renderer/library/components/layout/LibraryHeader.tsx index 9d617a0666..05aca60312 100644 --- a/src/renderer/library/components/layout/LibraryHeader.tsx +++ b/src/renderer/library/components/layout/LibraryHeader.tsx @@ -9,11 +9,9 @@ import { ipcRenderer } from "electron"; import * as stylesHeader from "readium-desktop/renderer/assets/styles/header.scss"; import * as stylesButtons from "readium-desktop/renderer/assets/styles/components/buttons.scss"; -import * as stylesModals from "readium-desktop/renderer/assets/styles/components/modals.scss"; import { useDispatch } from "readium-desktop/renderer/common/hooks/useDispatch"; import { screenReaderActions, toastActions } from "readium-desktop/common/redux/actions"; import { ToastType } from "readium-desktop/common/models/toast"; -import * as Dialog from "@radix-ui/react-dialog"; import { Link } from "react-router-dom"; import classNames from "classnames"; @@ -30,13 +28,11 @@ import SVG from "readium-desktop/renderer/common/components/SVG"; import { Settings } from "../settings/Settings"; import { _APP_NAME } from "readium-desktop/preprocessor-directives"; import { buildOpdsBrowserRoute } from "../../opds/route"; +import { buildCustomizationRoute } from "../../customization/route"; import { encodeURIComponent_RFC3986 } from "@r2-utils-js/_utils/http/UrlUtils"; import { URL_PROTOCOL_THORIUMHTTPS, URL_HOST_COMMON, URL_PATH_PREFIX_CUSTOMPROFILEZIP } from "readium-desktop/common/streamerProtocol"; -import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; -import DOMPurify from "dompurify"; import { useSelector } from "readium-desktop/renderer/common/hooks/useSelector"; import { useTranslator } from "readium-desktop/renderer/common/hooks/useTranslator"; -import { IStringMap } from "@r2-shared-js/models/metadata-multilang"; import { convertMultiLangStringToString } from "readium-desktop/common/language-string"; // import { WizardModal } from "../Wizard"; @@ -171,11 +167,6 @@ const Header = () => { } return a; }, [customizationManifest, locale]); - const screenZipObj = React.useMemo(() => screenZipLinks?.map(({ href, title }) => href && customizationBaseUrl ? {url: customizationBaseUrl + encodeURIComponent_RFC3986(Buffer.from(href).toString("base64")), title } : undefined), [screenZipLinks, customizationBaseUrl]); - - const [screenHtmlArray, setScreenHtmlArray] = React.useState>([]); - const [cancel, setCancel] = React.useState(false); - const screenReaderActivate = useSelector((state: ILibraryRootState) => state.screenReader.activate); @@ -215,51 +206,6 @@ const Header = () => { }; }, [isAccessibilitySupportEnabled]); - React.useEffect(() => { - if (customizationId && screenZipObj?.length) { - - setScreenHtmlArray([]); - setCancel(false); - - for (const screenHref of screenZipObj) { - - // URL is thoriumhttps:// "custom-profile-zip" protocol handler, so no use of isURL(url) and /^https?:\/\//.test(url) checks here - fetch(screenHref.url) - .then((response) => { - if (response.ok) { - return response.text(); - } - return Promise.reject(response.statusText); - }) - .then((rawHtmlContent) => { - // console.log("RAW HTML", rawHtmlContent); - - if (cancel) { - return ; - } - - if (!rawHtmlContent) { - return ; - } - - const htmlSanitized = DOMPurify.sanitize(rawHtmlContent, { FORBID_TAGS: [/*"style"*/], FORBID_ATTR: [/*"style"*/] /* TODO: handle external https links */ }); - // console.log(rawHtmlContent, htmlSanitized); - // NOTE that xxx is fine, caught by webContents.on("will-navigate", ...) with event.preventDefault() and shell.openExternal(...) on normalized/escaped URL and filtered on HTTP(S):// - // NOTE that the attribute target="_blank" (etc) is automatically removed by DOMPurify but would be caught by webContents.setWindowOpenHandler(...) with { action: "deny" }, although no shell.openExternal(...) in this case - setScreenHtmlArray((screenHtmlArray) => [...screenHtmlArray, { dangerousInnerHTML_CustomProfileScreenSanitized: htmlSanitized, title: screenHref.title }]); - }) - .catch((e) => { - console.error("Error fetching data:", e); - }); - } - } else { - setScreenHtmlArray([]); - setCancel(true); - } - - }, [screenZipObj, customizationId, setScreenHtmlArray, cancel, setCancel]); - - const customizationCatalogs = customizationManifest?.links?.filter(({ rel }) => rel === "catalog"); if (customizationCatalogs?.length) { for (const catalog of customizationCatalogs) { @@ -285,6 +231,25 @@ const Header = () => { }); } } + + if (screenZipLinks?.length) { + for (const screenLink of screenZipLinks) { + if (!screenLink.href) { + continue; + } + const route = buildCustomizationRoute(screenLink.href); + const label = convertMultiLangStringToString(screenLink.title, locale) || __("catalog.customization.fallback.screen"); + headerNav.push({ + route, + label, + matchRoutes: [route], + searchEnable: false, + styles: [], + svg: InfoIcon, + }); + } + } + const displayScreenReaderInvite = !screenReaderActivate && isAccessibilitySupportEnabled; return (<> @@ -347,43 +312,6 @@ const Header = () => { }, ) } - { - screenHtmlArray.length ? screenHtmlArray.map(({dangerousInnerHTML_CustomProfileScreenSanitized, title: titleStringOrObject}, index) => { - - const title = convertMultiLangStringToString(titleStringOrObject, locale); - - return <> -
  • - - - - - -
    - - { - // FALSE this to test sourcemaps: - true && - - {title || __("catalog.customization.fallback.screen")} - - } - - { - dangerousInnerHTML_CustomProfileScreenSanitized ? -
    : <> - } - - - - -
  • - ; - }) : <> - }
  • diff --git a/src/renderer/library/customization/route.ts b/src/renderer/library/customization/route.ts new file mode 100644 index 0000000000..ade6883f4a --- /dev/null +++ b/src/renderer/library/customization/route.ts @@ -0,0 +1,20 @@ +// ==LICENSE-BEGIN== +// Copyright 2017 European Digital Reading Lab. All rights reserved. +// Licensed to the Readium Foundation under one or more contributor license agreements. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file exposed on Github (readium) in the project repository. +// ==LICENSE-END== + +import { encodeB64, decodeB64 } from "../../common/logics/base64"; + +/** + * formating customization screen route to react-router route + * @param href href of the customization "screen" link (manifest.json) + */ +export function buildCustomizationRoute(href: string) { + return `/customization/${encodeB64(href)}`; +} + +export function decodeCustomizationRouteParam(hrefEncoded: string) { + return decodeB64(hrefEncoded); +} diff --git a/src/renderer/library/routing.ts b/src/renderer/library/routing.ts index 746b89de1d..6ab7b29c82 100644 --- a/src/renderer/library/routing.ts +++ b/src/renderer/library/routing.ts @@ -13,6 +13,7 @@ import Catalog from "./components/catalog/Catalog"; import Browser from "./components/opds/Browser"; import Opds from "./components/opds/Opds"; import AllPublicationPage from "./components/searchResult/AllPublicationPage"; +import CustomizationPage from "./components/customization/Customization"; // import TagSearchResult from "./components/searchResult/TagSearchResult"; // import TextSearchResult from "./components/searchResult/TextSearchResult"; @@ -103,6 +104,11 @@ const _routes = { // exact: true, component: Catalog, } as Route, + "/customization": { + path: "/customization/:hrefEncoded", + // exact: true, + component: CustomizationPage, + } as Route, "/": { path: "/", // exact: false, diff --git a/test/customization/profile/thorium_university_press/screen/faq-en.html b/test/customization/profile/thorium_university_press/screen/faq-en.html index aae9ed8e51..a3767ae55d 100644 --- a/test/customization/profile/thorium_university_press/screen/faq-en.html +++ b/test/customization/profile/thorium_university_press/screen/faq-en.html @@ -1,65 +1,318 @@ + - - - FAQ — Thorium University Library + + +FAQ — Thorium University Library + + + + + -

    FAQ — Thorium University Library

    -

    A simple list of frequently asked questions. No JavaScript, no specific styling.

    - -
      -
    • -

      What are the library's opening hours?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam non urna vitae augue bibendum.

      -
    • - -
    • -

      How can I borrow a book?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vel sapien nec justo cursus.

      -
    • - -
    • -

      What is the loan duration?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus euismod, justo at volutpat.

      -
    • - -
    • -

      How can I renew a loan?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed urna at sapien gravida.

      -
    • - -
    • -

      Are there late fees for overdue books?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis.

      -
    • - -
    • -

      How can I access online resources?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tristique leo sed massa luctus.

      -
    • - -
    • -

      Can I reserve a study room?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a arcu sit amet lacus.

      -
    • - -
    • -

      What are the rules for using computer stations?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean euismod, nibh et tincidunt.

      -
    • - -
    • -

      How can I contact the library staff?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet mi nec risus.

      -
    • - -
    • -

      Where can I find research documents and theses?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc pulvinar, lectus non convallis.

      -
    • -
    + +
    +
    +

    Frequently asked questions file

    +

    FAQ — Thorium University Library

    +

    Answers to the questions most often asked at the front desk and circulation counter.

    +
    +
    + +
    +
    + Classification 020 — Library Science + 10 cards +
    + +
    + + 020.1 + What are the opening hours? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam non urna vitae augue bibendum.

    +
    + +
    + + 020.2 + How do I borrow a book? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vel sapien nec justo cursus.

    +
    + +
    + + 020.3 + What is the loan period? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus euismod, justo at volutpat.

    +
    + +
    + + 020.4 + How do I renew a loan? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed urna at sapien gravida.

    +
    + +
    + + 020.5 + Are there late fees? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis.

    +
    + +
    + + 020.6 + How do I access online resources? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tristique leo sed massa luctus.

    +
    + +
    + + 020.7 + Can I book a study room? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a arcu sit amet lacus.

    +
    + +
    + + 020.8 + What are the rules for using computer stations? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean euismod, nibh et tincidunt.

    +
    + +
    + + 020.9 + How do I contact library staff? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet mi nec risus.

    +
    + +
    + + 020.10 + Where can I find research papers and theses? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc pulvinar, lectus non convallis.

    +
    +
    + +
    Thorium University Library
    - + \ No newline at end of file diff --git a/test/customization/profile/thorium_university_press/screen/faq-fr.html b/test/customization/profile/thorium_university_press/screen/faq-fr.html index 81a1a63601..5c38b80705 100644 --- a/test/customization/profile/thorium_university_press/screen/faq-fr.html +++ b/test/customization/profile/thorium_university_press/screen/faq-fr.html @@ -2,67 +2,317 @@ - - - FAQ — Bibliothèque Thorium University + + +FAQ — Bibliothèque Thorium University + + + + -

    FAQ — Bibliothèque Thorium University

    -

    Liste simple de questions fréquentes. Pas de JavaScript, pas de styles spécifiques.

    - -
      -
    • -

      Quels sont les horaires d'ouverture ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam non urna vitae augue bibendum.

      -
    • - -
    • -

      Comment emprunter un livre ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vel sapien nec justo cursus.

      -
    • - -
    • -

      Quelle est la durée du prêt ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus euismod, justo at volutpat.

      -
    • - -
    • -

      Comment renouveler un prêt ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed urna at sapien gravida.

      -
    • - -
    • -

      Y a-t-il des pénalités en cas de retard ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis.

      -
    • - -
    • -

      Comment accéder aux ressources en ligne ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tristique leo sed massa luctus.

      -
    • - -
    • -

      Puis-je réserver une salle d'étude ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a arcu sit amet lacus.

      -
    • - -
    • -

      Quelles sont les règles d'utilisation des postes informatiques ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean euismod, nibh et tincidunt.

      -
    • - -
    • -

      Comment contacter le personnel de la bibliothèque ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet mi nec risus.

      -
    • - -
    • -

      Où trouver les documents de recherche et thèses ?

      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc pulvinar, lectus non convallis.

      -
    • -
    - +
    +
    +

    Fichier des questions courantes

    +

    FAQ — Bibliothèque Thorium University

    +

    Les réponses aux questions les plus posées à l'accueil et au comptoir de prêt.

    +
    +
    + +
    +
    + Classification 020 — Bibliothéconomie + 10 fiches +
    + +
    + + 020.1 + Quels sont les horaires d'ouverture ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam non urna vitae augue bibendum.

    +
    + +
    + + 020.2 + Comment emprunter un livre ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vel sapien nec justo cursus.

    +
    + +
    + + 020.3 + Quelle est la durée du prêt ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus euismod, justo at volutpat.

    +
    +
    + + 020.4 + Comment renouveler un prêt ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed urna at sapien gravida.

    +
    + +
    + + 020.5 + Y a-t-il des pénalités en cas de retard ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis.

    +
    + +
    + + 020.6 + Comment accéder aux ressources en ligne ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tristique leo sed massa luctus.

    +
    + +
    + + 020.7 + Puis-je réserver une salle d'étude ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a arcu sit amet lacus.

    +
    + +
    + + 020.8 + Quelles sont les règles d'utilisation des postes informatiques ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean euismod, nibh et tincidunt.

    +
    + +
    + + 020.9 + Comment contacter le personnel de la bibliothèque ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet mi nec risus.

    +
    + +
    + + 020.10 + Où trouver les documents de recherche et thèses ? + + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc pulvinar, lectus non convallis.

    +
    +
    + +
    Bibliothèque Thorium University
    + + \ No newline at end of file From 5f1e9f0140805700ebb86912d5b6394bef941ea0 Mon Sep 17 00:00:00 2001 From: Pierre Leroux Date: Tue, 22 Sep 2026 15:55:42 +0200 Subject: [PATCH 02/11] Refactor renderer code and simplify state handling --- docs/customization-profile-screens.md | 14 + .../styles/components/profileScreen.scss | 57 +++ .../styles/components/profileScreen.scss.d.ts | 2 + src/renderer/library/analytics/pageView.ts | 39 +- .../customization/Customization.tsx | 243 +++++++++--- .../components/layout/LibraryHeader.tsx | 42 +- src/renderer/library/customization/route.ts | 79 +++- .../library/redux/sagas/customization.ts | 2 + src/renderer/library/redux/sagas/history.ts | 42 +- src/renderer/library/routing.ts | 4 +- .../screen/faq-en.html | 369 +++--------------- .../screen/faq-fr.html | 366 +++-------------- .../library/analytics/pageView.test.ts | 43 ++ .../library/customization/route.test.ts | 48 +++ 14 files changed, 635 insertions(+), 715 deletions(-) create mode 100644 docs/customization-profile-screens.md create mode 100644 src/renderer/assets/styles/components/profileScreen.scss create mode 100644 src/renderer/assets/styles/components/profileScreen.scss.d.ts create mode 100644 test/renderer/library/analytics/pageView.test.ts create mode 100644 test/renderer/library/customization/route.test.ts diff --git a/docs/customization-profile-screens.md b/docs/customization-profile-screens.md new file mode 100644 index 0000000000..0a04528c3a --- /dev/null +++ b/docs/customization-profile-screens.md @@ -0,0 +1,14 @@ +# Customization profile screens + +A customization manifest can add Library navigation pages with `rel: "screen"` links. Thorium displays the links matching the application locale; when none match, it uses English and language-neutral links in manifest order. + +Profile screen documents must follow this contract: + +- Use `text/html` (or omit the link type) and provide exactly one `

    `. +- Do not add a `
    ` landmark. Thorium owns the page `
    `; any nested `
    ` is converted to `
    `. +- Do not use scripts, frames, embedded objects, external stylesheets, `@import`, or `@font-face`. +- Inline ` + + + FAQ — Thorium University Library - - -
    -
    -

    Frequently asked questions file

    -

    FAQ — Thorium University Library

    -

    Answers to the questions most often asked at the front desk and circulation counter.

    -
    -
    - -
    -
    - Classification 020 — Library Science - 10 cards -
    - -
    - - 020.1 - What are the opening hours? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam non urna vitae augue bibendum.

    -
    - -
    - - 020.2 - How do I borrow a book? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vel sapien nec justo cursus.

    -
    - -
    - - 020.3 - What is the loan period? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus euismod, justo at volutpat.

    -
    - -
    - - 020.4 - How do I renew a loan? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed urna at sapien gravida.

    -
    - -
    - - 020.5 - Are there late fees? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis.

    -
    - -
    - - 020.6 - How do I access online resources? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tristique leo sed massa luctus.

    -
    - -
    - - 020.7 - Can I book a study room? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a arcu sit amet lacus.

    -
    - -
    - - 020.8 - What are the rules for using computer stations? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean euismod, nibh et tincidunt.

    -
    - -
    - - 020.9 - How do I contact library staff? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet mi nec risus.

    -
    - -
    - - 020.10 - Where can I find research papers and theses? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc pulvinar, lectus non convallis.

    -
    -
    - -
    Thorium University Library
    +

    FAQ — Thorium University Library

    +

    A simple list of frequently asked questions. No JavaScript, no specific styling.

    + +
      +
    • +

      What are the library's opening hours?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam non urna vitae augue bibendum.

      +
    • + +
    • +

      How can I borrow a book?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vel sapien nec justo cursus.

      +
    • + +
    • +

      What is the loan duration?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus euismod, justo at volutpat.

      +
    • + +
    • +

      How can I renew a loan?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed urna at sapien gravida.

      +
    • + +
    • +

      Are there late fees for overdue books?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis.

      +
    • + +
    • +

      How can I access online resources?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tristique leo sed massa luctus.

      +
    • + +
    • +

      Can I reserve a study room?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a arcu sit amet lacus.

      +
    • + +
    • +

      What are the rules for using computer stations?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean euismod, nibh et tincidunt.

      +
    • + +
    • +

      How can I contact the library staff?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet mi nec risus.

      +
    • + +
    • +

      Where can I find research documents and theses?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc pulvinar, lectus non convallis.

      +
    • +
    - \ No newline at end of file + diff --git a/test/customization/profile/thorium_university_press/screen/faq-fr.html b/test/customization/profile/thorium_university_press/screen/faq-fr.html index 5c38b80705..81a1a63601 100644 --- a/test/customization/profile/thorium_university_press/screen/faq-fr.html +++ b/test/customization/profile/thorium_university_press/screen/faq-fr.html @@ -2,317 +2,67 @@ - - -FAQ — Bibliothèque Thorium University - - - - + + + FAQ — Bibliothèque Thorium University - -
    -
    -

    Fichier des questions courantes

    -

    FAQ — Bibliothèque Thorium University

    -

    Les réponses aux questions les plus posées à l'accueil et au comptoir de prêt.

    -
    -
    - -
    -
    - Classification 020 — Bibliothéconomie - 10 fiches -
    - -
    - - 020.1 - Quels sont les horaires d'ouverture ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam non urna vitae augue bibendum.

    -
    - -
    - - 020.2 - Comment emprunter un livre ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vel sapien nec justo cursus.

    -
    - -
    - - 020.3 - Quelle est la durée du prêt ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus euismod, justo at volutpat.

    -
    - -
    - - 020.4 - Comment renouveler un prêt ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed urna at sapien gravida.

    -
    - -
    - - 020.5 - Y a-t-il des pénalités en cas de retard ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis.

    -
    - -
    - - 020.6 - Comment accéder aux ressources en ligne ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tristique leo sed massa luctus.

    -
    - -
    - - 020.7 - Puis-je réserver une salle d'étude ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a arcu sit amet lacus.

    -
    - -
    - - 020.8 - Quelles sont les règles d'utilisation des postes informatiques ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean euismod, nibh et tincidunt.

    -
    - -
    - - 020.9 - Comment contacter le personnel de la bibliothèque ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet mi nec risus.

    -
    - -
    - - 020.10 - Où trouver les documents de recherche et thèses ? - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc pulvinar, lectus non convallis.

    -
    -
    - -
    Bibliothèque Thorium University
    +

    FAQ — Bibliothèque Thorium University

    +

    Liste simple de questions fréquentes. Pas de JavaScript, pas de styles spécifiques.

    + +
      +
    • +

      Quels sont les horaires d'ouverture ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam non urna vitae augue bibendum.

      +
    • + +
    • +

      Comment emprunter un livre ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vel sapien nec justo cursus.

      +
    • + +
    • +

      Quelle est la durée du prêt ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus euismod, justo at volutpat.

      +
    • + +
    • +

      Comment renouveler un prêt ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sed urna at sapien gravida.

      +
    • + +
    • +

      Y a-t-il des pénalités en cas de retard ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis.

      +
    • + +
    • +

      Comment accéder aux ressources en ligne ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed tristique leo sed massa luctus.

      +
    • + +
    • +

      Puis-je réserver une salle d'étude ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a arcu sit amet lacus.

      +
    • + +
    • +

      Quelles sont les règles d'utilisation des postes informatiques ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean euismod, nibh et tincidunt.

      +
    • + +
    • +

      Comment contacter le personnel de la bibliothèque ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet mi nec risus.

      +
    • + +
    • +

      Où trouver les documents de recherche et thèses ?

      +

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc pulvinar, lectus non convallis.

      +
    • +
    + \ No newline at end of file diff --git a/test/renderer/library/analytics/pageView.test.ts b/test/renderer/library/analytics/pageView.test.ts new file mode 100644 index 0000000000..76812be8aa --- /dev/null +++ b/test/renderer/library/analytics/pageView.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "@jest/globals"; +import { ICustomizationManifest } from "readium-desktop/common/readium/customization/manifest"; +import { + buildLibraryPageViewParams, + libraryPageViewFromPathname, +} from "readium-desktop/renderer/library/analytics/pageView"; +import { buildProfileCatalogRootIdentifier } from "readium-desktop/renderer/library/customization/route"; + +const catalogHref = "https://example.com/catalog/root"; +const manifest = { + links: [{ rel: "catalog", href: catalogHref, properties: {} }], +} as unknown as ICustomizationManifest; + +describe("Library page_view analytics", () => { + it("uses a generic Profile Page title and a per-screen deduplication key", () => { + expect(libraryPageViewFromPathname("/profile/first")).toEqual({ + pageTitle: "Profile Page", + routeKey: "/profile/first", + }); + expect(libraryPageViewFromPathname("/profile/second")?.routeKey).not.toBe("/profile/first"); + }); + + it("recognizes every route below a manifest catalog without exposing its identity", () => { + const rootIdentifier = buildProfileCatalogRootIdentifier(catalogHref); + const pathname = `/opds/${rootIdentifier}/browse/2/title/feed`; + + expect(libraryPageViewFromPathname(pathname, manifest)).toEqual({ + pageTitle: "Profile Catalog", + routeKey: `Profile Catalog:${rootIdentifier}`, + }); + expect(buildLibraryPageViewParams("Profile Catalog")).toEqual({ + page_title: "Profile Catalog", + page_location: "https://desktop.thoriumreader.com/analytics/Profile%20Catalog", + }); + }); + + it("keeps non-profile catalogs generic", () => { + expect(libraryPageViewFromPathname("/opds/other/browse/1/title/feed", manifest)).toEqual({ + pageTitle: "Catalog", + routeKey: "Catalog", + }); + }); +}); diff --git a/test/renderer/library/customization/route.test.ts b/test/renderer/library/customization/route.test.ts new file mode 100644 index 0000000000..7b3db902c0 --- /dev/null +++ b/test/renderer/library/customization/route.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "@jest/globals"; +import { ICustomizationManifest } from "readium-desktop/common/readium/customization/manifest"; +import { + buildProfileRoute, + decodeProfileRouteParam, + getLocalizedProfileScreenLinks, + resolveProfileScreenLink, +} from "readium-desktop/renderer/library/customization/route"; + +const manifest = { + links: [ + { rel: "screen", href: "./faq-en.html", type: "text/html", language: "en", properties: {} }, + { rel: "screen", href: "./faq-fr.html", type: "text/html", language: "fr", properties: {} }, + { rel: "screen", href: "./neutral.html", type: "text/html", properties: {} }, + { rel: "screen", href: "./ignored.xhtml", type: "application/xhtml+xml", language: "fr", properties: {} }, + ], +} as unknown as ICustomizationManifest; + +describe("profile screen routes", () => { + it("builds a URL-safe /profile route and decodes it", () => { + const route = buildProfileRoute("./écran/faq.html?x=1"); + const screenId = route.replace("/profile/", ""); + + expect(route.startsWith("/profile/")).toBe(true); + expect(screenId).not.toMatch(/[+/=]/); + expect(decodeProfileRouteParam(screenId)).toBe("./écran/faq.html?x=1"); + }); + + it("does not throw for a malformed route parameter", () => { + expect(decodeProfileRouteParam("%%%invalid%%%")).toBeUndefined(); + }); + + it("selects exact-language screens before the English and neutral fallback", () => { + expect(getLocalizedProfileScreenLinks(manifest, "fr").map(({ href }) => href)).toEqual(["./faq-fr.html"]); + expect(getLocalizedProfileScreenLinks(manifest, "es").map(({ href }) => href)).toEqual([ + "./faq-en.html", + "./neutral.html", + ]); + }); + + it("only resolves a screen from the localized manifest allow-list", () => { + const frenchId = buildProfileRoute("./faq-fr.html").replace("/profile/", ""); + const englishId = buildProfileRoute("./faq-en.html").replace("/profile/", ""); + + expect(resolveProfileScreenLink(manifest, "fr", frenchId)?.href).toBe("./faq-fr.html"); + expect(resolveProfileScreenLink(manifest, "fr", englishId)).toBeUndefined(); + }); +}); From 69cef35b8ab28fbb23ea04c2050ddec7bb6b86f5 Mon Sep 17 00:00:00 2001 From: Pierre Leroux Date: Tue, 22 Sep 2026 16:25:48 +0200 Subject: [PATCH 03/11] Document profile CSS scoping and sanitization behavior --- .../customization/Customization.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/renderer/library/components/customization/Customization.tsx b/src/renderer/library/components/customization/Customization.tsx index 16007cec29..13a5783b8d 100644 --- a/src/renderer/library/components/customization/Customization.tsx +++ b/src/renderer/library/components/customization/Customization.tsx @@ -33,6 +33,10 @@ type TProfileScreenState = const PROFILE_SCREEN_SCOPE_SELECTOR = ".custom-profile-screen"; +// A selector list cannot be split with String.split(",") because commas are +// also valid inside constructs such as :is(...), :not(...), and attribute +// selectors. Track bracket and parenthesis depth so only top-level commas +// separate selectors. const splitSelectorList = (selectorText: string): string[] => { const selectors: string[] = []; let currentSelector = ""; @@ -58,6 +62,11 @@ const splitSelectorList = (selectorText: string): string[] => { }; const selectorTargetsProfileScreen = (selector: string): boolean => { + // Remove an optional `body` or `body[...]` ancestor followed by whitespace. + // For example, `body[data-theme="dark"] .custom-profile-screen h1` becomes + // `.custom-profile-screen h1`. This permits theme-qualified rules while the + // check below still requires the actual style target to be the profile + // container or one of its descendants. const normalizedSelector = selector.trim().replace(/^body(?:\[[^\]]+\])?\s+/, ""); return normalizedSelector === PROFILE_SCREEN_SCOPE_SELECTOR || [" ", ".", "#", ":", "[", ">", "+", "~"].some( @@ -65,6 +74,11 @@ const selectorTargetsProfileScreen = (selector: string): boolean => { ); }; +// Walk every parsed CSS rule, including rules nested in @media, @supports, or +// similar grouping rules. Each style selector must explicitly target the +// stable profile container. Keyframe declarations are accepted because they +// do not select DOM nodes and can only take effect when referenced by an +// already-scoped style rule. const profileCssRulesAreScoped = (rules: CSSRuleList): boolean => Array.from(rules).every((rule) => { if ("selectorText" in rule && typeof rule.selectorText === "string") { @@ -79,11 +93,18 @@ const profileCssRulesAreScoped = (rules: CSSRuleList): boolean => }); const profileCssIsSafeAndScoped = (cssText: string): boolean => { + // DOMPurify sanitizes markup and element attributes, but it does not + // provide stylesheet isolation. Block CSS that can load another + // stylesheet or font, plus legacy executable URL forms, before parsing. if (/@(?:font-face|import)\b/i.test(cssText) || /(?:expression|url)\s*\(\s*["']?\s*javascript:/i.test(cssText)) { return false; } try { + // Let Chromium's CSS parser interpret the stylesheet instead of using + // regular expressions for complete CSS syntax. Invalid stylesheets or + // selectors that escape the profile root cause the document to be + // rejected rather than risking changes to Thorium's Library UI. const sheet = new CSSStyleSheet(); sheet.replaceSync(cssText); return profileCssRulesAreScoped(sheet.cssRules); @@ -93,8 +114,13 @@ const profileCssIsSafeAndScoped = (cssText: string): boolean => { }; export function prepareProfileScreenHtml(rawHtmlContent: string): string | undefined { - // Profile packages are controlled and signed. Their CSS may be retained, but the - // authoring contract requires every selector to be scoped below .custom-profile-screen. + // Profile packages are controlled and signed, so the page remains in the + // regular DOM instead of a Shadow DOM. This preserves Thorium's theme, + // focus management, accessibility landmarks, and link handling. Isolation + // is enforced by sanitizing the HTML and requiring retained stylesheet + // selectors to remain below .custom-profile-screen. Inline style + // attributes are local to their element, while Thorium's !important rules + // still enforce the application font and standard text sizes. const sanitizedHtml = DOMPurify.sanitize(rawHtmlContent, { FORCE_BODY: true, FORBID_ATTR: ["srcdoc"], From 8cb872164db4905597e9901e14f194bc4bb99139 Mon Sep 17 00:00:00 2001 From: Pierre Leroux Date: Tue, 22 Sep 2026 16:48:36 +0200 Subject: [PATCH 04/11] Harden profile screen CSS validation --- docs/customization-profile-screens.md | 47 ++++- .../customization/Customization.tsx | 83 +------- src/renderer/library/customization/style.ts | 192 ++++++++++++++++++ .../library/customization/style.test.ts | 138 +++++++++++++ 4 files changed, 377 insertions(+), 83 deletions(-) create mode 100644 src/renderer/library/customization/style.ts create mode 100644 test/renderer/library/customization/style.test.ts diff --git a/docs/customization-profile-screens.md b/docs/customization-profile-screens.md index 0a04528c3a..fb74350163 100644 --- a/docs/customization-profile-screens.md +++ b/docs/customization-profile-screens.md @@ -6,9 +6,54 @@ Profile screen documents must follow this contract: - Use `text/html` (or omit the link type) and provide exactly one `

    `. - Do not add a `
    ` landmark. Thorium owns the page `
    `; any nested `
    ` is converted to `
    `. -- Do not use scripts, frames, embedded objects, external stylesheets, `@import`, or `@font-face`. +- Do not use scripts, frames, embedded objects, external stylesheets, `@import`, `@font-face`, `@keyframes`, or `@page`. - Inline `