diff --git a/.taskfiles/backend.yml b/.taskfiles/backend.yml index dde75433a81..8a0127eff16 100644 --- a/.taskfiles/backend.yml +++ b/.taskfiles/backend.yml @@ -15,9 +15,16 @@ version: '3' # stripping; cmd.exe also requires `.\` (not bare `gradlew.bat`) # because modern Windows excludes cwd from cmd's search path. +vars: + GRADLE: '{{if eq OS "windows"}}cmd /c ".\gradlew.bat"{{else}}./gradlew{{end}}' + TEST: '{{.GRADLE}} test --no-daemon' + FORMAT: '{{.GRADLE}} spotlessApply' + FORMATCHECK: '{{.GRADLE}} spotlessCheck' + CLEAN: '{{.GRADLE}} clean' + tasks: dev: - desc: "Start backend dev server" + desc: "Start the backend dev server" cmds: - task: dev:proprietary vars: @@ -28,7 +35,7 @@ tasks: SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}' dev:proprietary: - desc: "Start backend dev server in proprietary mode" + desc: "Start the backend dev server in proprietary mode" # `dotenv:` reads from the root Taskfile's directory (".") because this # subtaskfile is included with `dir: .`. Local overrides in # .env.proprietary.local win over the committed .env.proprietary defaults. @@ -43,22 +50,16 @@ tasks: env: SERVER_PORT: '{{.PORT}}' cmds: - - cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"' - platforms: [windows] - - cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}./gradlew :stirling-pdf:bootRun' - platforms: [linux, darwin] + - '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{.GRADLE}} :stirling-pdf:bootRun' dev:bundled: - desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)" + desc: "Start the backend with the frontend bundled into the JAR" ignore_error: true cmds: - - cmd: cmd /c ".\gradlew.bat clean bootRun -PbuildWithFrontend=true" - platforms: [windows] - - cmd: ./gradlew clean bootRun -PbuildWithFrontend=true - platforms: [linux, darwin] + - '{{.CLEAN}} bootRun -PbuildWithFrontend=true' dev:saas: - desc: "Start backend in SaaS flavor against Supabase" + desc: "Start the backend in SaaS flavor" # `dotenv:` reads from the root Taskfile's directory (".") because this # subtaskfile is included with `dir: .`. dotenv: ['app/.env.saas.local', 'app/.env.saas'] @@ -77,71 +78,104 @@ tasks: AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}' AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}' cmds: - - cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}" - platforms: [windows] - - cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILES}}--args='--spring.profiles.include={{.PROFILES}}'{{end}} - platforms: [linux, darwin] + - '{{.GRADLE}} :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}' build: - desc: "Full backend build" + desc: "Build the backend" cmds: - - cmd: cmd /c ".\gradlew.bat clean build" - platforms: [windows] - - cmd: ./gradlew clean build - platforms: [linux, darwin] + - '{{.CLEAN}} build' build:fast: - desc: "Build without tests" + desc: "Build the backend without running tests" cmds: - - cmd: cmd /c ".\gradlew.bat clean build -x test" - platforms: [windows] - - cmd: ./gradlew clean build -x test - platforms: [linux, darwin] + - '{{.CLEAN}} build -x test' build:ci: - desc: "Build for CI (formatting checked separately)" + desc: "Build the backend for CI" cmds: - - cmd: cmd /c ".\gradlew.bat build -PnoSpotless" - platforms: [windows] - - cmd: ./gradlew build -PnoSpotless - platforms: [linux, darwin] + - '{{.GRADLE}} build -PnoSpotless' + + prematrix:*: + desc: "Run the backend test matrix" + vars: + MATRIXNAME: '{{index .MATCH 0}}' + cmds: + - for: + matrix: + FLAVOR: ["proprietary", "core", "saas"] + LOGIN: ["true", "false"] + SEC: ["true", "false"] + task: '{{.MATRIXNAME}}:matrix' + vars: + STIRLING_FLAVOR: '{{.ITEM.FLAVOR}}' + SECURITY_ENABLELOGIN: '{{.ITEM.LOGIN}}' + DOCKER_ENABLE_SECURITY: '{{.ITEM.SEC}}' test: - desc: "Run backend tests" + desc: "Run the backend test matrix" cmds: - - cmd: cmd /c ".\gradlew.bat test" - platforms: [windows] - - cmd: ./gradlew test - platforms: [linux, darwin] + - task: prematrix:test + + test:matrix: + internal: true + env: + SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}' + STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}' + DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}' + desc: "Run backend tests for one configuration" + # Cover the backend with the main build/property combinations that change + # Gradle behavior in this repo. + cmds: + - '{{.TEST}}' + - '{{.TEST}} -PbuildWithFrontend=true' + - '{{.TEST}} -PprototypesMode=true' + - '{{.TEST}} -PbuildWithFrontend=true -PprototypesMode=true' format: - desc: "Auto-fix code formatting" + desc: "Run the backend formatting matrix" cmds: - - cmd: cmd /c ".\gradlew.bat spotlessApply" - platforms: [windows] - - cmd: ./gradlew spotlessApply - platforms: [linux, darwin] + - task: prematrix:format + + format:matrix: + internal: true + desc: "Apply backend formatting for one configuration" + env: + SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}' + STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}' + DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}' + cmds: + - '{{.FORMAT}}' + - '{{.FORMAT}} -PbuildWithFrontend=true' + - '{{.FORMAT}} -PprototypesMode=true' format:check: - desc: "Check code formatting" + desc: "Check the backend formatting matrix" cmds: - - cmd: cmd /c ".\gradlew.bat spotlessCheck" - platforms: [windows] - - cmd: ./gradlew spotlessCheck - platforms: [linux, darwin] + - task: prematrix:format:check + + format:check:matrix: + internal: true + desc: "Check backend formatting for one configuration" + env: + SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}' + STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}' + DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}' + # Mirror the test matrix so formatting checks use the same backend + # configuration variants. + cmds: + - '{{.FORMATCHECK}}' + - '{{.FORMATCHECK}} -PbuildWithFrontend=true' + - '{{.FORMATCHECK}} -PprototypesMode=true' fix: - desc: "Auto-fix backend" + desc: "Apply backend fixes" cmds: - task: format swagger: - desc: "Generate OpenAPI docs" + desc: "Generate the backend OpenAPI docs" cmds: - - cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc" - platforms: [windows] - - cmd: ./gradlew :stirling-pdf:copySwaggerDoc - platforms: [linux, darwin] + - '{{.GRADLE}} :stirling-pdf:copySwaggerDoc' sources: - app/core/src/main/java/**/*.java - app/proprietary/src/main/java/**/*.java @@ -150,40 +184,43 @@ tasks: - SwaggerDoc.json check: - desc: "Backend quality gate" + desc: "Run the backend quality gate" cmds: - task: format:check - task: test version: - desc: "Print project version" + desc: "Print the backend version" silent: true cmds: - - cmd: cmd /c ".\gradlew.bat printVersion --quiet" | tail -1 + - cmd: pwsh -NoProfile -File scripts/backend-version.ps1 platforms: [windows] - cmd: ./gradlew printVersion --quiet | tail -1 platforms: [linux, darwin] licenses:check: - desc: "Check dependency licenses" + desc: "Check backend dependency licenses" cmds: - - cmd: cmd /c ".\gradlew.bat checkLicense --no-parallel" - platforms: [windows] - - cmd: ./gradlew checkLicense --no-parallel - platforms: [linux, darwin] + - '{{.GRADLE}} checkLicense --no-parallel' licenses:generate: - desc: "Check and generate dependency license report" + desc: "Generate the backend dependency license report" + env: + # Use the SaaS flavor so the license report includes all dependencies. + STIRLING_FLAVOR: 'saas' cmds: - - cmd: cmd /c ".\gradlew.bat checkLicense generateLicenseReport --no-parallel" - platforms: [windows] - - cmd: ./gradlew checkLicense generateLicenseReport --no-parallel + - '{{.GRADLE}} checkLicense generateLicenseReport --no-parallel' + + licenses:generate:copy: + desc: "Generate and copy the backend dependency license report" + deps: [licenses:generate] + cmds: + - cmd: cp build/reports/dependency-license/index.json app/core/src/main/resources/static/3rdPartyLicenses.json platforms: [linux, darwin] + - cmd: powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path 'app/core/src/main/resources/static' | Out-Null; Copy-Item -Force 'build/reports/dependency-license/index.json' 'app/core/src/main/resources/static/3rdPartyLicenses.json'" + platforms: [windows] clean: - desc: "Clean build artifacts" + desc: "Clean backend build artifacts" cmds: - - cmd: cmd /c ".\gradlew.bat clean" - platforms: [windows] - - cmd: ./gradlew clean - platforms: [linux, darwin] + - '{{.CLEAN}}' diff --git a/app/common/build.gradle b/app/common/build.gradle index 516edd4897b..416f54c3405 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -6,7 +6,7 @@ spotless { java { target 'src/**/java/**/*.java' targetExclude 'src/main/java/org/apache/**' - googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false) + googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false) // google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25 suppressLintsFor { setStep('google-java-format') } @@ -29,19 +29,19 @@ spotless { } } dependencies { - api "com.google.guava:guava:${guavaVersion}" + api "com.google.guava:guava:${rootProject.ext.guavaVersion}" api 'org.springframework.boot:spring-boot-starter-webmvc' api 'org.springframework.boot:spring-boot-starter-aspectj' api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1' api 'com.fathzer:javaluator:3.0.6' api 'com.posthog.java:posthog:1.2.0' - api "org.apache.commons:commons-lang3:${commonsLang3}" + api "org.apache.commons:commons-lang3:${rootProject.ext.commonsLang3}" api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8' - api "org.apache.pdfbox:pdfbox:$pdfboxVersion" - api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion" - api "org.apache.pdfbox:xmpbox:$pdfboxVersion" - api "org.apache.pdfbox:preflight:$pdfboxVersion" + api "org.apache.pdfbox:pdfbox:${rootProject.ext.pdfboxVersion}" + api "org.apache.pdfbox:pdfbox-io:${rootProject.ext.pdfboxVersion}" + api "org.apache.pdfbox:xmpbox:${rootProject.ext.pdfboxVersion}" + api "org.apache.pdfbox:preflight:${rootProject.ext.pdfboxVersion}" api 'com.github.junrar:junrar:7.5.10' // RAR archive support for CBR files api 'jakarta.servlet:jakarta.servlet-api:6.1.0' api 'org.snakeyaml:snakeyaml-engine:3.0.1' @@ -60,7 +60,7 @@ dependencies { exclude group: 'com.google.code.gson', module: 'gson' } - api "com.stirling:jpdfium:${jpdfiumVersion}" + api "com.stirling:jpdfium:${rootProject.ext.jpdfiumVersion}" // -PjpdfiumPlatforms=all| def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim() @@ -75,12 +75,12 @@ dependencies { } logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}") jpdfiumPlatforms.each { platform -> - runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}" + runtimeOnly "com.stirling:jpdfium-natives-${platform}:${rootProject.ext.jpdfiumVersion}" } // Bucket4j (local in-process token bucket for RateLimitStore default impl) - implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}" + implementation "com.bucket4j:bucket4j_jdk17-core:${rootProject.ext.bucket4jVersion}" // ArchUnit: enforces module dependency direction (see ArchitectureTest) - testImplementation "com.tngtech.archunit:archunit-junit5:${archunitVersion}" + testImplementation "com.tngtech.archunit:archunit-junit5:${rootProject.ext.archunitVersion}" } diff --git a/app/core/.prettierignore b/app/core/.prettierignore new file mode 100644 index 00000000000..3b0924781b8 --- /dev/null +++ b/app/core/.prettierignore @@ -0,0 +1,2 @@ +# Auto-generated by MSW (`msw init`); regenerated verbatim, not hand-formatted. +src/main/resources/static/mockServiceWorker.js \ No newline at end of file diff --git a/app/core/build.gradle b/app/core/build.gradle index 6a55d893045..9914ae4aeee 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -13,7 +13,7 @@ spotless { java { target 'src/**/java/**/*.java' targetExclude 'src/main/resources/static/**', 'src/main/java/org/apache/**' - googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false) + googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false) // google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25 suppressLintsFor { setStep('google-java-format') } @@ -67,21 +67,21 @@ dependencies { exclude group: 'com.fasterxml.jackson.jaxrs' exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-jaxb-annotations' } - implementation "commons-io:commons-io:$commonsIoVersion" - implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" - implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" + implementation "commons-io:commons-io:${rootProject.ext.commonsIoVersion}" + implementation "org.bouncycastle:bcprov-jdk18on:${rootProject.ext.bouncycastleVersion}" + implementation "org.bouncycastle:bcpkix-jdk18on:${rootProject.ext.bouncycastleVersion}" implementation 'io.micrometer:micrometer-core' implementation 'com.google.zxing:core:3.5.4' - implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark - implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion" + implementation "org.commonmark:commonmark:${rootProject.ext.commonmarkVersion}" // https://mvnrepository.com/artifact/org.commonmark/commonmark + implementation "org.commonmark:commonmark-ext-gfm-tables:${rootProject.ext.commonmarkVersion}" // General PDF dependencies - implementation "org.apache.pdfbox:preflight:$pdfboxVersion" - implementation "org.apache.pdfbox:xmpbox:$pdfboxVersion" + implementation "org.apache.pdfbox:preflight:${rootProject.ext.pdfboxVersion}" + implementation "org.apache.pdfbox:xmpbox:${rootProject.ext.pdfboxVersion}" implementation 'org.verapdf:validation-model:1.28.2' // CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13 - implementation "org.mozilla:rhino:${rhinoVersion}" + implementation "org.mozilla:rhino:${rootProject.ext.rhinoVersion}" // veraPDF still uses javax.xml.bind, not the new jakarta namespace implementation 'javax.xml.bind:jaxb-api:2.3.1' @@ -89,33 +89,33 @@ dependencies { implementation 'com.sun.xml.bind:jaxb-core:4.0.7' // CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7) - implementation "com.google.code.gson:gson:${gsonVersion}" + implementation "com.google.code.gson:gson:${rootProject.ext.gsonVersion}" implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4' implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv implementation 'org.apache.poi:poi-ooxml:5.5.1' // Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom) // Replaces batik-all which included unused codec, svggen, transcoder, script modules - implementation "org.apache.xmlgraphics:batik-bridge:${batikVersion}" + implementation "org.apache.xmlgraphics:batik-bridge:${rootProject.ext.batikVersion}" // Required by TwelveMonkeys imageio-batik SPI (SVGImageReaderSpi) during ImageIO init - runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${batikVersion}" + runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${rootProject.ext.batikVersion}" // PDFBox Graphics2D bridge for Batik SVG to PDF conversion implementation 'de.rototor.pdfbox:graphics2d:3.0.5' // TwelveMonkeys - runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion" - runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion" - runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion" - runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion" - runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion" + runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:${rootProject.ext.imageioVersion}" + runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:${rootProject.ext.imageioVersion}" + runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:${rootProject.ext.imageioVersion}" + runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:${rootProject.ext.imageioVersion}" + runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:${rootProject.ext.imageioVersion}" // runtimeOnly "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@ // runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion" - runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion" + runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:${rootProject.ext.imageioVersion}" // runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion" @@ -135,7 +135,6 @@ sourceSets { } - // Disable regular jar jar { enabled = false @@ -292,7 +291,11 @@ tasks.register('npmBuild', Exec) { group = 'frontend' description = 'Build editor frontend application' workingDir file('../..') - commandLine = ['task', frontendBuildTask] + // Pin the repo-root Taskfile explicitly so the Exec task does not inherit + // an unrelated TASKFILE override from the surrounding environment. + commandLine = Os.isFamily(Os.FAMILY_WINDOWS) ? + ['cmd', '/c', 'task', '--taskfile', 'Taskfile.yml', frontendBuildTask] : + ['task', '--taskfile', 'Taskfile.yml', frontendBuildTask] inputs.dir(new File(frontendEditorDir, 'src')) inputs.dir(new File(frontendEditorDir, 'public')) inputs.file(new File(frontendDir, 'package.json')) diff --git a/app/core/src/main/resources/static/css/cookieconsentCustomisation.css b/app/core/src/main/resources/static/css/cookieconsentCustomisation.css index fd1a8ff3555..25ec0fd5275 100644 --- a/app/core/src/main/resources/static/css/cookieconsentCustomisation.css +++ b/app/core/src/main/resources/static/css/cookieconsentCustomisation.css @@ -20,17 +20,18 @@ --cc-separator-border-color: #e0e0e0; - --cc-toggle-on-bg: #007bff; - --cc-toggle-off-bg: #667481; - --cc-toggle-on-knob-bg: #ffffff; - --cc-toggle-off-knob-bg: #ffffff; + /* Toggle colors mirror Mantine Switch (light scheme) */ + --cc-toggle-on-bg: var(--mantine-primary-color-filled, #007bff); + --cc-toggle-off-bg: var(--mantine-color-gray-3, #dee2e6); + --cc-toggle-on-knob-bg: var(--mantine-color-white, #ffffff); + --cc-toggle-off-knob-bg: var(--mantine-color-white, #ffffff); --cc-toggle-enabled-icon-color: #ffffff; --cc-toggle-disabled-icon-color: #ffffff; - --cc-toggle-readonly-bg: #f1f3f4; - --cc-toggle-readonly-knob-bg: #79747e; - --cc-toggle-readonly-knob-icon-color: #f1f3f4; + --cc-toggle-readonly-bg: var(--mantine-color-disabled, #f1f3f4); + --cc-toggle-readonly-knob-bg: var(--mantine-color-gray-0, #f8f9fa); + --cc-toggle-readonly-knob-icon-color: transparent; --cc-section-category-border: #e0e0e0; @@ -69,17 +70,18 @@ --cc-separator-border-color: #555555; - --cc-toggle-on-bg: #4dabf7; - --cc-toggle-off-bg: #667481; - --cc-toggle-on-knob-bg: #2d2d2d; - --cc-toggle-off-knob-bg: #2d2d2d; + /* Toggle colors mirror Mantine Switch (dark scheme) */ + --cc-toggle-on-bg: var(--mantine-primary-color-filled, #4dabf7); + --cc-toggle-off-bg: var(--mantine-color-dark-5, #555555); + --cc-toggle-on-knob-bg: var(--mantine-color-white, #ffffff); + --cc-toggle-off-knob-bg: var(--mantine-color-white, #ffffff); --cc-toggle-enabled-icon-color: #2d2d2d; --cc-toggle-disabled-icon-color: #2d2d2d; - --cc-toggle-readonly-bg: #555555; - --cc-toggle-readonly-knob-bg: #8e8e8e; - --cc-toggle-readonly-knob-icon-color: #555555; + --cc-toggle-readonly-bg: var(--mantine-color-disabled, #555555); + --cc-toggle-readonly-knob-bg: var(--mantine-color-dark-3, #8e8e8e); + --cc-toggle-readonly-knob-icon-color: transparent; --cc-section-category-border: #555555; @@ -176,9 +178,16 @@ color: var(--cc-primary-color) !important; } -/* Lower z-index so cookie banner appears behind onboarding modals */ +/* Banner sits above the chat FAB but behind all modals and onboarding; value + is Z_INDEX_COOKIE_CONSENT_BANNER, set as this variable by useCookieConsent */ #cc-main { - z-index: 100 !important; + z-index: var(--z-index-cookie-consent) !important; +} + +/* Preferences dialog sits above the settings modal it opens from; value is + Z_INDEX_COOKIE_PREFERENCES_MODAL, set as this variable by useCookieConsent */ +.show--preferences #cc-main { + z-index: var(--z-index-cookie-preferences) !important; } /* Ensure consent modal text is visible in both themes */ @@ -203,3 +212,63 @@ #cc-main .cm__link { color: var(--cc-primary-color) !important; } + +/* ── Category toggles restyled to match Mantine Switch (size sm) ────────── + Mantine sm metrics: 38×20 track, 14px plain thumb, 2.5px inline padding, + 150ms ease transitions, no icon inside the thumb. Colors come from the + --cc-toggle-* variables above, which point at the Mantine palette. */ +#cc-main .section__toggle, +#cc-main .section__toggle-wrapper, +#cc-main .toggle__icon, +#cc-main .toggle__label { + width: 38px !important; + height: 20px !important; + border-radius: 1000px !important; +} + +/* Track: flat fill, no outline ring or border */ +#cc-main .toggle__icon { + border: none !important; + box-shadow: none !important; + transition: background-color 150ms ease !important; +} + +#cc-main .section__toggle:checked ~ .toggle__icon { + border: none !important; + box-shadow: none !important; +} + +/* Always-enabled categories = Mantine disabled switch (must out-prioritise + the !important checked-track rule above) */ +#cc-main .section__toggle:checked:disabled ~ .toggle__icon { + background: var(--cc-toggle-readonly-bg) !important; + border: none !important; + box-shadow: none !important; +} + +#cc-main .section__toggle:disabled { + cursor: not-allowed !important; +} + +/* Thumb: small plain circle, vertically centred, no drop shadow */ +#cc-main .toggle__icon-circle { + width: 14px !important; + height: 14px !important; + top: 3px !important; + left: 2.5px !important; + box-shadow: none !important; + transition: + transform 150ms ease, + background-color 150ms ease !important; +} + +/* Checked thumb travel: 38 − 14 − 2.5 = 21.5px end position */ +#cc-main .section__toggle:checked ~ .toggle__icon .toggle__icon-circle { + transform: translateX(19px) !important; +} + +/* Mantine switches have no check/cross glyph inside the thumb */ +#cc-main .toggle__icon-on, +#cc-main .toggle__icon-off { + display: none !important; +} diff --git a/app/core/src/main/resources/static/mockServiceWorker.js b/app/core/src/main/resources/static/mockServiceWorker.js new file mode 100644 index 00000000000..33dde9e7700 --- /dev/null +++ b/app/core/src/main/resources/static/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.14.6' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/app/core/src/test/java/stirling/software/SPDF/utils/SvgToPdfTest.java b/app/core/src/test/java/stirling/software/SPDF/utils/SvgToPdfTest.java index 63c3c73ffcf..a44dd2f0228 100644 --- a/app/core/src/test/java/stirling/software/SPDF/utils/SvgToPdfTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/utils/SvgToPdfTest.java @@ -147,11 +147,17 @@ void convert_doesNotEmbedExternalFileResource() throws Exception { ImageIO.write(red, "png", external.toFile()); try { + String rootRelativeExternal = + "/" + + external.toAbsolutePath() + .toString() + .replace('\\', '/') + .replaceFirst("^/+", ""); String svg = "" + ""; byte[] pdf; diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index 60f6541c823..39b0c94b3fd 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -11,7 +11,7 @@ spotless { java { target 'src/**/java/**/*.java' targetExclude 'src/main/java/org/apache/**' - googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false) + googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false) // google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25 suppressLintsFor { setStep('google-java-format') } @@ -35,13 +35,13 @@ spotless { } dependencies { implementation project(':common') - api "com.google.guava:guava:${guavaVersion}" + api "com.google.guava:guava:${rootProject.ext.guavaVersion}" api 'org.springframework:spring-jdbc' api 'org.springframework:spring-webmvc' api 'org.springframework.session:spring-session-core' - api "org.springframework.security:spring-security-core:$springSecuritySamlVersion" - api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion" + api "org.springframework.security:spring-security-core:${rootProject.ext.springSecuritySamlVersion}" + api "org.springframework.security:spring-security-saml2-service-provider:${rootProject.ext.springSecuritySamlVersion}" api 'org.springframework.boot:spring-boot-starter-jetty' api 'org.springframework.boot:spring-boot-starter-security' api 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -55,16 +55,16 @@ dependencies { api 'com.github.ben-manes.caffeine:caffeine' implementation 'org.springframework.boot:spring-boot-starter-data-redis' api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46' - implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}" + implementation "com.bucket4j:bucket4j_jdk17-core:${rootProject.ext.bucket4jVersion}" // Lettuce-backed Bucket4j ProxyManager used by ValkeyRateLimitStore for cluster-wide // token-bucket rate limiting (parity with in-process Bucket4j semantics; no fixed-window // boundary doubling). - implementation "com.bucket4j:bucket4j_jdk17-lettuce:${bucket4jVersion}" + implementation "com.bucket4j:bucket4j_jdk17-lettuce:${rootProject.ext.bucket4jVersion}" // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 - implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" + implementation "org.bouncycastle:bcprov-jdk18on:${rootProject.ext.bouncycastleVersion}" - implementation "com.google.code.gson:gson:${gsonVersion}" + implementation "com.google.code.gson:gson:${rootProject.ext.gsonVersion}" // jinjava/jjwt transitively request older Jackson 2 versions; declare the current // version directly so it is selected consistently (root build.gradle pins are the fallback). @@ -82,17 +82,17 @@ dependencies { api 'io.micrometer:micrometer-registry-prometheus' - api "io.jsonwebtoken:jjwt-api:${jwtVersion}" - runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}" - runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}" + api "io.jsonwebtoken:jjwt-api:${rootProject.ext.jwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-impl:${rootProject.ext.jwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-jackson:${rootProject.ext.jwtVersion}" runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database - file format incompatible with 2.4.x, would break existing user databases runtimeOnly 'org.postgresql:postgresql:42.7.11' implementation('com.coveo:saml-client:5.0.0') { exclude group: 'org.opensaml', module: 'opensaml-core' } - implementation "software.amazon.awssdk:s3:${awsSdkVersion}" - implementation "software.amazon.awssdk:url-connection-client:${awsSdkVersion}" + implementation "software.amazon.awssdk:s3:${rootProject.ext.awsSdkVersion}" + implementation "software.amazon.awssdk:url-connection-client:${rootProject.ext.awsSdkVersion}" // @DataJpaTest slice (Boot 4 ships test slices as separate starters, like webmvc-test at the // root) so policy.source repositories can be exercised against embedded H2. @@ -100,11 +100,10 @@ dependencies { // Testcontainers: real MinIO/LocalStack (S3) and Valkey for integration tests in CI without // manually-started instances. Tests skip cleanly when Docker is unavailable. - testImplementation "org.testcontainers:testcontainers:${testcontainersMinioVersion}" - testImplementation "org.testcontainers:minio:${testcontainersMinioVersion}" - testImplementation "org.testcontainers:localstack:${testcontainersMinioVersion}" - testImplementation "org.testcontainers:postgresql:${testcontainersMinioVersion}" - testImplementation "org.testcontainers:junit-jupiter:${testcontainersMinioVersion}" + testImplementation "org.testcontainers:testcontainers:${rootProject.ext.testcontainersMinioVersion}" + testImplementation "org.testcontainers:minio:${rootProject.ext.testcontainersMinioVersion}" + testImplementation "org.testcontainers:localstack:${rootProject.ext.testcontainersMinioVersion}" + testImplementation "org.testcontainers:junit-jupiter:${rootProject.ext.testcontainersMinioVersion}" } tasks.register('prepareKotlinBuildScriptModel') {} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyAuthIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyAuthIntegrationTest.java index 133ef35f6c3..1f5b37c483b 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyAuthIntegrationTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyAuthIntegrationTest.java @@ -24,7 +24,7 @@ * the full production bean method {@code valkeyConnectionFactory()} so the parse, credential * wiring, and eager-handshake all run exactly as at boot. */ -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) @EnabledIf("isDockerAvailable") class LiveValkeyAuthIntegrationTest { diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyChaosTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyChaosTest.java index f619272c16a..36d8ff11c82 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyChaosTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyChaosTest.java @@ -28,7 +28,7 @@ * to reproduce a partition rather than {@code stop} (which would fail fast with * connection-refused). */ -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) @EnabledIf("isDockerAvailable") class LiveValkeyChaosTest { diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java index c26528a7159..8a4c3815cbb 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java @@ -39,7 +39,7 @@ * unavailable - without that guard, {@code @Testcontainers} would throw {@code initializationError} * (test FAILURE, not skip) on CI runners without Docker. */ -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) @EnabledIf("isDockerAvailable") class LiveValkeyIntegrationTest { diff --git a/build.gradle b/build.gradle index e2f47e26fbf..484fbec2232 100644 --- a/build.gradle +++ b/build.gradle @@ -235,6 +235,7 @@ subprojects { resolutionStrategy.force "org.bouncycastle:bcprov-jdk18on:${bouncycastleVersion}" resolutionStrategy.force "org.bouncycastle:bcpkix-jdk18on:${bouncycastleVersion}" resolutionStrategy.force "org.bouncycastle:bcutil-jdk18on:${bouncycastleVersion}" + resolutionStrategy.force "org.apache.santuario:xmlsec:3.0.5" } dependencyManagement { @@ -285,6 +286,7 @@ subprojects { def jacocoReport = tasks.named("jacocoTestReport") tasks.withType(Test).configureEach { + jvmArgs "--enable-native-access=ALL-UNNAMED" useJUnitPlatform() finalizedBy(jacocoReport) } @@ -297,7 +299,8 @@ subprojects { html.required.set(true) } doLast { - def xmlReport = reports.xml.outputLocation.get().asFile + def reportBaseDir = layout.buildDirectory.dir("reports/jacoco/test").get().asFile + def xmlReport = new File(reportBaseDir, "jacocoTestReport.xml") if (!xmlReport.exists()) { logger.lifecycle("Jacoco coverage report not found at ${xmlReport}") return @@ -376,7 +379,7 @@ subprojects { } logger.lifecycle(separator) - def htmlReport = reports.html.outputLocation.get().asFile + def htmlReport = new File(reportBaseDir, "html") logger.lifecycle("Detailed HTML report available at: ${htmlReport}") if (rows.any { it[3] == "FAIL" }) { logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.") @@ -666,7 +669,7 @@ tasks.register('compileRestartHelper', JavaCompile) { source = fileTree(dir: 'scripts', include: 'RestartHelper.java') classpath = files() destinationDirectory = layout.buildDirectory.dir("restart-helper-classes") - def restartMajorVersion = project.ext.modernJavaVersion + def restartMajorVersion = rootProject.ext.modernJavaVersion def restartCompatibility = JavaVersion.toVersion(restartMajorVersion.toString()) sourceCompatibility = restartCompatibility targetCompatibility = restartCompatibility diff --git a/scripts/backend-version.ps1 b/scripts/backend-version.ps1 new file mode 100644 index 00000000000..87c3089944f --- /dev/null +++ b/scripts/backend-version.ps1 @@ -0,0 +1,9 @@ +$ErrorActionPreference = 'Stop' + +$output = & "$PSScriptRoot/../gradlew.bat" printVersion --quiet 2>&1 +$lines = @($output | Where-Object { $_.ToString().Trim() }) +if ($lines.Count -eq 0) { + exit 0 +} + +Write-Output $lines[-1]